9#import <OCMock/OCMock.h>
10#import <XCTest/XCTest.h>
24@property(nonatomic, copy) NSString* autofillId;
25- (void)setEditableTransform:(NSArray*)matrix;
26- (void)setTextInputClient:(
int)client;
27- (void)setTextInputState:(NSDictionary*)state;
28- (void)setMarkedRect:(CGRect)markedRect;
29- (void)updateEditingState;
30- (
BOOL)isVisibleToAutofill;
31- (
id<FlutterTextInputDelegate>)textInputDelegate;
32- (void)configureWithDictionary:(NSDictionary*)configuration;
33- (void)handleSearchWebAction;
34- (void)handleLookUpAction;
35- (void)handleShareAction;
43- (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target;
50- (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
55- (
BOOL)accessibilityElementIsFocused {
56 return _isAccessibilityFocused;
62@property(nonatomic, strong) UITextField*
textField;
67@property(nonatomic, readonly) UIView* inputHider;
68@property(nonatomic, readonly) UIView* keyboardViewContainer;
69@property(nonatomic, readonly) UIView* keyboardView;
70@property(nonatomic, assign) UIView* cachedFirstResponder;
71@property(nonatomic, readonly) CGRect keyboardRect;
72@property(nonatomic, readonly)
BOOL pendingInputHiderRemoval;
73@property(nonatomic, readonly)
74 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
76- (void)cleanUpViewHierarchy:(
BOOL)includeActiveView
77 clearText:(
BOOL)clearText
78 delayRemoval:(
BOOL)delayRemoval;
79- (NSArray<UIView*>*)textInputViews;
82- (void)startLiveTextInput;
83- (void)showKeyboardAndRemoveScreenshot;
89class MockPlatformViewDelegate :
public PlatformView::Delegate {
91 void OnPlatformViewCreated(std::unique_ptr<Surface>
surface)
override {}
92 void OnPlatformViewDestroyed()
override {}
93 void OnPlatformViewScheduleFrame()
override {}
94 void OnPlatformViewAddView(int64_t
view_id,
95 const ViewportMetrics& viewport_metrics,
96 AddViewCallback
callback)
override {}
97 void OnPlatformViewRemoveView(int64_t
view_id, RemoveViewCallback
callback)
override {}
98 void OnPlatformViewSendViewFocusEvent(
const ViewFocusEvent& event)
override {};
99 void OnPlatformViewSetNextFrameCallback(
const fml::closure& closure)
override {}
100 void OnPlatformViewSetViewportMetrics(int64_t
view_id,
const ViewportMetrics& metrics)
override {}
102 void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage>
message)
override {}
103 void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet)
override {
105 void OnPlatformViewDispatchSemanticsAction(int64_t
view_id,
109 void OnPlatformViewSetSemanticsEnabled(
bool enabled)
override {}
110 void OnPlatformViewSetAccessibilityFeatures(int32_t flags)
override {}
111 void OnPlatformViewRegisterTexture(std::shared_ptr<Texture>
texture)
override {}
112 void OnPlatformViewUnregisterTexture(int64_t
texture_id)
override {}
113 void OnPlatformViewMarkTextureFrameAvailable(int64_t
texture_id)
override {}
115 void LoadDartDeferredLibrary(intptr_t loading_unit_id,
116 std::unique_ptr<const fml::Mapping> snapshot_data,
117 std::unique_ptr<const fml::Mapping> snapshot_instructions)
override {
119 void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
120 const std::string error_message,
121 bool transient)
override {}
122 void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
135 NSDictionary* _template;
153 UIPasteboard.generalPasteboard.items = @[];
159 [textInputPlugin.autofillContext removeAllObjects];
160 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
161 [[[[textInputPlugin textInputView] superview] subviews]
162 makeObjectsPerformSelector:@selector(removeFromSuperview)];
167- (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
170 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
171 [textInputPlugin handleMethodCall:setClientCall
172 result:^(id _Nullable result){
176- (void)setClientClear {
179 [textInputPlugin handleMethodCall:clearClientCall
180 result:^(id _Nullable result){
184- (void)setTextInputShow {
187 [textInputPlugin handleMethodCall:setClientCall
188 result:^(id _Nullable result){
192- (void)setTextInputHide {
195 [textInputPlugin handleMethodCall:setClientCall
196 result:^(id _Nullable result){
200- (void)flushScheduledAsyncBlocks {
201 __block
bool done =
false;
202 XCTestExpectation* expectation =
203 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
204 dispatch_async(dispatch_get_main_queue(), ^{
207 dispatch_async(dispatch_get_main_queue(), ^{
209 [expectation fulfill];
211 [
self waitForExpectations:@[ expectation ] timeout:10];
214- (NSMutableDictionary*)mutableTemplateCopy {
217 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
218 @"keyboardAppearance" :
@"Brightness.light",
219 @"obscureText" : @NO,
220 @"inputAction" :
@"TextInputAction.unspecified",
221 @"smartDashesType" :
@"0",
222 @"smartQuotesType" :
@"0",
223 @"autocorrect" : @YES,
224 @"enableInteractiveSelection" : @YES,
228 return [_template mutableCopy];
232 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
233 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
237- (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
238 atIndex:(NSInteger)index {
241 withGranularity:UITextGranularityLine
242 inDirection:UITextLayoutDirectionRight];
247- (void)updateConfig:(NSDictionary*)config {
250 [textInputPlugin handleMethodCall:updateConfigCall
251 result:^(id _Nullable result){
257- (void)testWillNotCrashWhenViewControllerIsNil {
264 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
267 result:^(id _Nullable result) {
268 XCTAssertNil(result);
269 [expectation fulfill];
271 XCTAssertNil(inputPlugin.activeView);
272 [
self waitForExpectations:@[ expectation ] timeout:1.0];
275- (void)testInvokeStartLiveTextInput {
280 result:^(id _Nullable result){
282 OCMVerify([mockPlugin startLiveTextInput]);
285- (void)testNoDanglingEnginePointer {
295 weakFlutterEngine = flutterEngine;
296 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
298 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
299 weakFlutterTextInputPlugin = flutterTextInputPlugin;
303 NSDictionary* config =
self.mutableTemplateCopy;
306 arguments:@[ [NSNumber numberWithInt:123], config ]];
308 result:^(id _Nullable result){
310 currentView = flutterTextInputPlugin.activeView;
313 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
314 XCTAssertNotNil(currentView,
@"current view must not be nil");
316 XCTAssertNil(weakFlutterTextInputPlugin);
319 XCTAssertNil(currentView.textInputDelegate);
322- (void)testSecureInput {
323 NSDictionary* config =
self.mutableTemplateCopy;
324 [config setValue:@"YES" forKey:@"obscureText"];
325 [
self setClientId:123 configuration:config];
328 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
335 XCTAssertTrue(inputView.secureTextEntry);
338 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
341 XCTAssertEqual(inputFields.count, 1ul);
349 XCTAssert(inputView.autofillId.length > 0);
352- (void)testKeyboardType {
353 NSDictionary* config =
self.mutableTemplateCopy;
354 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
355 [
self setClientId:123 configuration:config];
358 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
363 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
366- (void)testKeyboardTypeWebSearch {
367 NSDictionary* config =
self.mutableTemplateCopy;
368 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
369 [
self setClientId:123 configuration:config];
372 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
377 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
380- (void)testKeyboardTypeTwitter {
381 NSDictionary* config =
self.mutableTemplateCopy;
382 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
383 [
self setClientId:123 configuration:config];
386 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
391 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
394- (void)testVisiblePasswordUseAlphanumeric {
395 NSDictionary* config =
self.mutableTemplateCopy;
396 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
397 [
self setClientId:123 configuration:config];
400 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
405 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
408- (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
409 NSDictionary* config =
self.mutableTemplateCopy;
410 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
411 [
self setClientId:123 configuration:config];
416 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
417 [
self setClientId:124 configuration:config];
422- (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
426 if (@available(iOS 17.0, *)) {
428 OCMVerify(never(), [
engine flutterTextInputView:inputView
429 showAutocorrectionPromptRectForStart:0
433 OCMVerify([
engine flutterTextInputView:inputView
434 showAutocorrectionPromptRectForStart:0
440- (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
442 __block
int updateCount = 0;
443 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
444 .andDo(^(NSInvocation* invocation) {
448 [inputView.text setString:@"Some initial text"];
449 XCTAssertEqual(updateCount, 0);
452 [inputView setSelectedTextRange:textRange];
453 XCTAssertEqual(updateCount, 1);
456 NSDictionary* config =
self.mutableTemplateCopy;
457 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
458 [config setValue:@(NO) forKey:@"obscureText"];
459 [config setValue:@(NO) forKey:@"enableDeltaModel"];
460 [inputView configureWithDictionary:config];
463 [inputView setSelectedTextRange:textRange];
465 XCTAssertEqual(updateCount, 1);
468- (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
470 if (@available(iOS 17.0, *)) {
474 if (@available(iOS 14.0, *)) {
477 __block
int callCount = 0;
478 OCMStub([
engine flutterTextInputView:inputView
479 showAutocorrectionPromptRectForStart:0
482 .andDo(^(NSInvocation* invocation) {
488 XCTAssertEqual(callCount, 1);
490 UIScribbleInteraction* scribbleInteraction =
491 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
493 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
497 XCTAssertEqual(callCount, 1);
499 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
500 [inputView resetScribbleInteractionStatusIfEnding];
503 XCTAssertEqual(callCount, 2);
505 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
509 XCTAssertEqual(callCount, 2);
511 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
515 XCTAssertEqual(callCount, 2);
517 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
520 XCTAssertEqual(callCount, 3);
524- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
530 arguments:@[ @(123),
self.mutableTemplateCopy ]];
532 result:^(id _Nullable result){
539 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
543 arguments:@{@"transform" : yOffsetMatrix}];
545 result:^(id _Nullable result){
548 if (@available(iOS 17, *)) {
549 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
550 @"The input hider should overlap with the text on and after iOS 17");
553 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
554 @"The input hider should be on the origin of screen on and before iOS 16.");
558- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
564 toPosition:toPosition];
565 NSRange range = flutterRange.
range;
567 XCTAssertEqual(range.location, 0ul);
568 XCTAssertEqual(range.length, 2ul);
571- (void)testTextInRange {
572 NSDictionary* config =
self.mutableTemplateCopy;
573 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
574 [
self setClientId:123 configuration:config];
575 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
578 [inputView insertText:@"test"];
581 NSString* substring = [inputView textInRange:range];
582 XCTAssertEqual(substring.length, 4ul);
585 substring = [inputView textInRange:range];
586 XCTAssertEqual(substring.length, 0ul);
589- (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
590 NSDictionary* config =
self.mutableTemplateCopy;
591 [
self setClientId:123 configuration:config];
592 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
595 [inputView insertText:@"text"];
598 NSString* substring = [inputView textInRange:range];
599 XCTAssertNil(substring);
602- (void)testStandardEditActions {
603 NSDictionary* config =
self.mutableTemplateCopy;
604 [
self setClientId:123 configuration:config];
605 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
608 [inputView insertText:@"aaaa"];
609 [inputView selectAll:nil];
611 [inputView insertText:@"bbbb"];
612 XCTAssertTrue([inputView canPerformAction:@selector(paste:) withSender:nil]);
613 [inputView paste:nil];
614 [inputView selectAll:nil];
615 [inputView copy:nil];
616 [inputView paste:nil];
617 [inputView selectAll:nil];
618 [inputView delete:nil];
619 [inputView paste:nil];
620 [inputView paste:nil];
623 NSString* substring = [inputView textInRange:range];
624 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
627- (void)testCanPerformActionForSelectActions {
628 NSDictionary* config =
self.mutableTemplateCopy;
629 [
self setClientId:123 configuration:config];
630 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
633 XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
635 [inputView insertText:@"aaaa"];
637 XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
640- (void)testCanPerformActionCaptureTextFromCamera {
641 if (@available(iOS 15.0, *)) {
642 NSDictionary* config =
self.mutableTemplateCopy;
643 [
self setClientId:123 configuration:config];
644 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
647 [inputView becomeFirstResponder];
648 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
650 [inputView insertText:@"test"];
651 [inputView selectAll:nil];
652 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
656- (void)testDeletingBackward {
657 NSDictionary* config =
self.mutableTemplateCopy;
658 [
self setClientId:123 configuration:config];
659 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
662 [inputView insertText:@"ឹ😀 text 🥰👨👩👧👦🇺🇳ดี "];
663 [inputView deleteBackward];
664 [inputView deleteBackward];
667 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
668 [inputView deleteBackward];
669 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
670 [inputView deleteBackward];
671 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
672 [inputView deleteBackward];
673 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
674 [inputView deleteBackward];
676 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
677 [inputView deleteBackward];
678 [inputView deleteBackward];
679 [inputView deleteBackward];
680 [inputView deleteBackward];
681 [inputView deleteBackward];
682 [inputView deleteBackward];
684 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
685 [inputView deleteBackward];
686 XCTAssertEqualObjects(inputView.text,
@"ឹ");
687 [inputView deleteBackward];
688 XCTAssertEqualObjects(inputView.text,
@"");
693- (void)testSystemOnlyAddingPartialComposedCharacter {
694 NSDictionary* config =
self.mutableTemplateCopy;
695 [
self setClientId:123 configuration:config];
696 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
699 [inputView insertText:@"👨👩👧👦"];
700 [inputView deleteBackward];
703 [inputView insertText:[@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)]];
704 [inputView insertText:@"아"];
706 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
709 [inputView deleteBackward];
712 [inputView insertText:@"😀"];
713 [inputView deleteBackward];
715 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
716 [inputView insertText:@"아"];
717 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
720 [inputView deleteBackward];
723 [inputView deleteBackward];
725 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
726 [inputView insertText:@"아"];
728 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
731- (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
732 NSDictionary* config =
self.mutableTemplateCopy;
733 [
self setClientId:123 configuration:config];
734 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
737 [inputView insertText:@"👨👩👧👦"];
738 [inputView deleteBackward];
739 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
742 NSString* brokenEmoji = [@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)];
743 [inputView insertText:brokenEmoji];
744 [inputView insertText:@"아"];
746 NSString* finalText = [NSString stringWithFormat:@"%@아", brokenEmoji];
747 XCTAssertEqualObjects(inputView.text, finalText);
750- (void)testPastingNonTextDisallowed {
751 NSDictionary* config =
self.mutableTemplateCopy;
752 [
self setClientId:123 configuration:config];
753 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
756 UIPasteboard.generalPasteboard.color = UIColor.redColor;
757 XCTAssertNil(UIPasteboard.generalPasteboard.string);
758 XCTAssertFalse([inputView canPerformAction:@selector(paste:) withSender:nil]);
759 [inputView paste:nil];
761 XCTAssertEqualObjects(inputView.text,
@"");
764- (void)testNoZombies {
771 [passwordView.textField description];
773 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
776- (void)testInputViewCrash {
781 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
782 activeView = inputPlugin.activeView;
784 [activeView updateEditingState];
787- (void)testDoNotReuseInputViews {
788 NSDictionary* config =
self.mutableTemplateCopy;
789 [
self setClientId:123 configuration:config];
791 [
self setClientId:456 configuration:config];
793 XCTAssertNotNil(currentView);
798- (void)ensureOnlyActiveViewCanBecomeFirstResponder {
800 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
804- (void)testPropagatePressEventsToViewController {
806 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
807 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
811 NSDictionary* config =
self.mutableTemplateCopy;
812 [
self setClientId:123 configuration:config];
814 [
self setTextInputShow];
816 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
817 withEvent:OCMClassMock([UIPressesEvent class])];
819 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
820 withEvent:[OCMArg isNotNil]]);
821 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
822 withEvent:[OCMArg isNotNil]]);
824 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
825 withEvent:OCMClassMock([UIPressesEvent class])];
827 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
828 withEvent:[OCMArg isNotNil]]);
829 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
830 withEvent:[OCMArg isNotNil]]);
833- (void)testPropagatePressEventsToViewController2 {
835 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
836 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
840 NSDictionary* config =
self.mutableTemplateCopy;
841 [
self setClientId:123 configuration:config];
842 [
self setTextInputShow];
845 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
846 withEvent:OCMClassMock([UIPressesEvent class])];
848 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
849 withEvent:[OCMArg isNotNil]]);
850 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
851 withEvent:[OCMArg isNotNil]]);
854 [
self setClientId:321 configuration:config];
855 [
self setTextInputShow];
857 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
859 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
860 withEvent:OCMClassMock([UIPressesEvent class])];
862 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
863 withEvent:[OCMArg isNotNil]]);
864 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
865 withEvent:[OCMArg isNotNil]]);
868- (void)testHotRestart {
869 flutter::MockPlatformViewDelegate mock_platform_view_delegate;
870 auto thread = std::make_unique<fml::Thread>(
"TextInputHotRestart");
871 auto thread_task_runner = thread->GetTaskRunner();
877 id mockFlutterView = OCMClassMock([
FlutterView class]);
880 OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
881 OCMStub([mockFlutterViewController
textInputPlugin]).andReturn(mockFlutterTextInputPlugin);
884 thread_task_runner->PostTask([&] {
885 auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
886 mock_platform_view_delegate,
887 mock_platform_view_delegate.settings_.enable_impeller
893 std::make_shared<
fml::SyncSwitch>());
895 platform_view->SetOwnerViewController(mockFlutterViewController);
897 OCMExpect([mockFlutterTextInputPlugin reset]);
899 OCMVerifyAll(mockFlutterView);
906- (void)testUpdateSecureTextEntry {
907 NSDictionary* config =
self.mutableTemplateCopy;
908 [config setValue:@"YES" forKey:@"obscureText"];
909 [
self setClientId:123 configuration:config];
911 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
914 __block
int callCount = 0;
915 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
919 XCTAssertTrue(inputView.isSecureTextEntry);
921 config =
self.mutableTemplateCopy;
922 [config setValue:@"NO" forKey:@"obscureText"];
923 [
self updateConfig:config];
925 XCTAssertEqual(callCount, 1);
926 XCTAssertFalse(inputView.isSecureTextEntry);
929- (void)testInputActionContinueAction {
945 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
947 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
950- (void)testDisablingAutocorrectDisablesSpellChecking {
954 NSDictionary* config =
self.mutableTemplateCopy;
955 [inputView configureWithDictionary:config];
957 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
958 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
960 [config setValue:@(NO) forKey:@"autocorrect"];
961 [inputView configureWithDictionary:config];
963 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
964 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
967- (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
969 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
983 XCTAssertEqual(inputView.markedTextRange, nil);
986- (void)testFlutterTextInputViewIsNotClearWhenKeyboardShowAndHide {
989 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
990 XCTAssertEqualObjects(inputView.text,
@"test text");
993 [
self setTextInputShow];
994 XCTAssertEqualObjects(inputView.text,
@"test text");
997 [
self setTextInputHide];
998 XCTAssertEqualObjects(inputView.text,
@"test text");
1001- (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
1005 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
1006 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
1007 if (@available(iOS 17, *)) {
1008 XCTAssertFalse(respondsToInsertionPointColor);
1010 XCTAssertTrue(respondsToInsertionPointColor);
1014#pragma mark - TextEditingDelta tests
1015- (void)testTextEditingDeltasAreGeneratedOnTextInput {
1017 inputView.enableDeltaModel = YES;
1019 __block
int updateCount = 0;
1021 [inputView insertText:@"text to insert"];
1024 flutterTextInputView:inputView
1025 updateEditingClient:0
1026 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1027 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1028 isEqualToString:
@""]) &&
1029 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1030 isEqualToString:
@"text to insert"]) &&
1031 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1032 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
1034 .andDo(^(NSInvocation* invocation) {
1037 XCTAssertEqual(updateCount, 0);
1039 [
self flushScheduledAsyncBlocks];
1042 XCTAssertEqual(updateCount, 1);
1044 [inputView deleteBackward];
1045 OCMExpect([
engine flutterTextInputView:inputView
1046 updateEditingClient:0
1047 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1048 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1049 isEqualToString:
@"text to insert"]) &&
1050 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1051 isEqualToString:
@""]) &&
1052 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1054 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1057 .andDo(^(NSInvocation* invocation) {
1060 [
self flushScheduledAsyncBlocks];
1061 XCTAssertEqual(updateCount, 2);
1064 OCMExpect([
engine flutterTextInputView:inputView
1065 updateEditingClient:0
1066 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1067 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1068 isEqualToString:
@"text to inser"]) &&
1069 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1070 isEqualToString:
@""]) &&
1071 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1073 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1076 .andDo(^(NSInvocation* invocation) {
1079 [
self flushScheduledAsyncBlocks];
1080 XCTAssertEqual(updateCount, 3);
1083 withText:@"replace text"];
1086 flutterTextInputView:inputView
1087 updateEditingClient:0
1088 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1089 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1090 isEqualToString:
@"text to inser"]) &&
1091 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1092 isEqualToString:
@"replace text"]) &&
1093 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1094 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
1096 .andDo(^(NSInvocation* invocation) {
1099 [
self flushScheduledAsyncBlocks];
1100 XCTAssertEqual(updateCount, 4);
1102 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1103 OCMExpect([
engine flutterTextInputView:inputView
1104 updateEditingClient:0
1105 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1106 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1107 isEqualToString:
@"replace textext to inser"]) &&
1108 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1109 isEqualToString:
@"marked text"]) &&
1110 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1112 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1115 .andDo(^(NSInvocation* invocation) {
1118 [
self flushScheduledAsyncBlocks];
1119 XCTAssertEqual(updateCount, 5);
1121 [inputView unmarkText];
1123 flutterTextInputView:inputView
1124 updateEditingClient:0
1125 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1126 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1127 isEqualToString:
@"replace textmarked textext to inser"]) &&
1128 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1129 isEqualToString:
@""]) &&
1130 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1132 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1135 .andDo(^(NSInvocation* invocation) {
1138 [
self flushScheduledAsyncBlocks];
1140 XCTAssertEqual(updateCount, 6);
1144- (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1147 inputView.enableDeltaModel = YES;
1150 OCMExpect([
engine flutterTextInputView:inputView
1151 updateEditingClient:0
1152 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1153 NSArray* deltas = state[@"deltas"];
1154 NSDictionary* firstDelta = deltas[0];
1155 NSDictionary* secondDelta = deltas[1];
1156 NSDictionary* thirdDelta = deltas[2];
1157 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1158 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1159 [firstDelta[@"deltaStart"] intValue] == 0 &&
1160 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1161 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1162 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1163 [secondDelta[@"deltaStart"] intValue] == 0 &&
1164 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1165 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1166 [thirdDelta[@"deltaText"] isEqualToString:@"—"] &&
1167 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1168 [thirdDelta[@"deltaEnd"] intValue] == 0;
1172 [inputView insertText:@"-"];
1173 [inputView deleteBackward];
1174 [inputView insertText:@"—"];
1176 [
self flushScheduledAsyncBlocks];
1180- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1182 inputView.enableDeltaModel = YES;
1184 __block
int updateCount = 0;
1185 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1186 .andDo(^(NSInvocation* invocation) {
1190 [inputView.text setString:@"Some initial text"];
1191 XCTAssertEqual(updateCount, 0);
1194 inputView.markedTextRange = range;
1195 inputView.selectedTextRange = nil;
1196 [
self flushScheduledAsyncBlocks];
1197 XCTAssertEqual(updateCount, 1);
1199 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1201 flutterTextInputView:inputView
1202 updateEditingClient:0
1203 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1204 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1205 isEqualToString:
@"Some initial text"]) &&
1206 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1207 isEqualToString:
@"new marked text."]) &&
1208 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1209 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1211 [
self flushScheduledAsyncBlocks];
1212 XCTAssertEqual(updateCount, 2);
1215- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1217 inputView.enableDeltaModel = YES;
1219 __block
int updateCount = 0;
1220 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1221 .andDo(^(NSInvocation* invocation) {
1225 [inputView.text setString:@"Some initial text"];
1226 [
self flushScheduledAsyncBlocks];
1227 XCTAssertEqual(updateCount, 0);
1230 inputView.markedTextRange = range;
1231 inputView.selectedTextRange = nil;
1232 [
self flushScheduledAsyncBlocks];
1233 XCTAssertEqual(updateCount, 1);
1235 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1237 flutterTextInputView:inputView
1238 updateEditingClient:0
1239 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1240 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1241 isEqualToString:
@"Some initial text"]) &&
1242 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1243 isEqualToString:
@"text."]) &&
1244 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1245 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1247 [
self flushScheduledAsyncBlocks];
1248 XCTAssertEqual(updateCount, 2);
1251- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1253 inputView.enableDeltaModel = YES;
1255 __block
int updateCount = 0;
1256 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1257 .andDo(^(NSInvocation* invocation) {
1261 [inputView.text setString:@"Some initial text"];
1262 [
self flushScheduledAsyncBlocks];
1263 XCTAssertEqual(updateCount, 0);
1266 inputView.markedTextRange = range;
1267 inputView.selectedTextRange = nil;
1268 [
self flushScheduledAsyncBlocks];
1269 XCTAssertEqual(updateCount, 1);
1271 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1273 flutterTextInputView:inputView
1274 updateEditingClient:0
1275 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1276 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1277 isEqualToString:
@"Some initial text"]) &&
1278 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1279 isEqualToString:
@"tex"]) &&
1280 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1281 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1283 [
self flushScheduledAsyncBlocks];
1284 XCTAssertEqual(updateCount, 2);
1287#pragma mark - EditingState tests
1289- (void)testUITextInputCallsUpdateEditingStateOnce {
1292 __block
int updateCount = 0;
1293 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1294 .andDo(^(NSInvocation* invocation) {
1298 [inputView insertText:@"text to insert"];
1300 XCTAssertEqual(updateCount, 1);
1302 [inputView deleteBackward];
1303 XCTAssertEqual(updateCount, 2);
1306 XCTAssertEqual(updateCount, 3);
1309 withText:@"replace text"];
1310 XCTAssertEqual(updateCount, 4);
1312 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1313 XCTAssertEqual(updateCount, 5);
1315 [inputView unmarkText];
1316 XCTAssertEqual(updateCount, 6);
1319- (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1321 inputView.enableDeltaModel = YES;
1323 __block
int updateCount = 0;
1324 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1325 .andDo(^(NSInvocation* invocation) {
1329 [inputView insertText:@"text to insert"];
1330 [
self flushScheduledAsyncBlocks];
1332 XCTAssertEqual(updateCount, 1);
1334 [inputView deleteBackward];
1335 [
self flushScheduledAsyncBlocks];
1336 XCTAssertEqual(updateCount, 2);
1339 [
self flushScheduledAsyncBlocks];
1340 XCTAssertEqual(updateCount, 3);
1343 withText:@"replace text"];
1344 [
self flushScheduledAsyncBlocks];
1345 XCTAssertEqual(updateCount, 4);
1347 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1348 [
self flushScheduledAsyncBlocks];
1349 XCTAssertEqual(updateCount, 5);
1351 [inputView unmarkText];
1352 [
self flushScheduledAsyncBlocks];
1353 XCTAssertEqual(updateCount, 6);
1356- (void)testTextChangesDoNotTriggerUpdateEditingClient {
1359 __block
int updateCount = 0;
1360 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1361 .andDo(^(NSInvocation* invocation) {
1365 [inputView.text setString:@"BEFORE"];
1366 XCTAssertEqual(updateCount, 0);
1368 inputView.markedTextRange = nil;
1369 inputView.selectedTextRange = nil;
1370 XCTAssertEqual(updateCount, 1);
1373 XCTAssertEqual(updateCount, 1);
1374 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1375 XCTAssertEqual(updateCount, 1);
1376 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1377 XCTAssertEqual(updateCount, 1);
1381 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1382 XCTAssertEqual(updateCount, 1);
1384 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1385 XCTAssertEqual(updateCount, 1);
1389 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1390 XCTAssertEqual(updateCount, 1);
1392 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1393 XCTAssertEqual(updateCount, 1);
1396- (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1398 inputView.enableDeltaModel = YES;
1400 __block
int updateCount = 0;
1401 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1402 .andDo(^(NSInvocation* invocation) {
1406 [inputView.text setString:@"BEFORE"];
1407 [
self flushScheduledAsyncBlocks];
1408 XCTAssertEqual(updateCount, 0);
1410 inputView.markedTextRange = nil;
1411 inputView.selectedTextRange = nil;
1412 [
self flushScheduledAsyncBlocks];
1413 XCTAssertEqual(updateCount, 1);
1416 XCTAssertEqual(updateCount, 1);
1417 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1418 [
self flushScheduledAsyncBlocks];
1419 XCTAssertEqual(updateCount, 1);
1421 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1422 [
self flushScheduledAsyncBlocks];
1423 XCTAssertEqual(updateCount, 1);
1427 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1428 [
self flushScheduledAsyncBlocks];
1429 XCTAssertEqual(updateCount, 1);
1432 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1433 [
self flushScheduledAsyncBlocks];
1434 XCTAssertEqual(updateCount, 1);
1438 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1439 [
self flushScheduledAsyncBlocks];
1440 XCTAssertEqual(updateCount, 1);
1443 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1444 [
self flushScheduledAsyncBlocks];
1445 XCTAssertEqual(updateCount, 1);
1448- (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1451 __block
int updateCount = 0;
1452 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1453 .andDo(^(NSInvocation* invocation) {
1457 [inputView unmarkText];
1459 XCTAssertEqual(updateCount, 0);
1461 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1463 XCTAssertEqual(updateCount, 1);
1465 [inputView unmarkText];
1467 XCTAssertEqual(updateCount, 2);
1470- (void)testCanCopyPasteWithScribbleEnabled {
1471 if (@available(iOS 14.0, *)) {
1472 NSDictionary* config =
self.mutableTemplateCopy;
1473 [
self setClientId:123 configuration:config];
1474 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1480 [mockInputView insertText:@"aaaa"];
1481 [mockInputView selectAll:nil];
1483 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1484 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1485 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1486 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1488 [mockInputView copy:NULL];
1489 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1490 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1491 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1492 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1496- (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1497 if (@available(iOS 14.0, *)) {
1500 __block
int updateCount = 0;
1501 OCMStub([
engine flutterTextInputView:inputView
1502 updateEditingClient:0
1503 withState:[OCMArg isNotNil]])
1504 .andDo(^(NSInvocation* invocation) {
1508 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1510 XCTAssertEqual(updateCount, 1);
1512 UIScribbleInteraction* scribbleInteraction =
1513 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1515 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1516 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1518 XCTAssertEqual(updateCount, 1);
1520 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1521 [inputView resetScribbleInteractionStatusIfEnding];
1522 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1524 XCTAssertEqual(updateCount, 2);
1526 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1527 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1530 XCTAssertEqual(updateCount, 2);
1532 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1533 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1536 XCTAssertEqual(updateCount, 2);
1538 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1539 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1541 XCTAssertEqual(updateCount, 3);
1545- (void)testUpdateEditingClientNegativeSelection {
1548 [inputView.text setString:@"SELECTION"];
1549 inputView.markedTextRange = nil;
1550 inputView.selectedTextRange = nil;
1552 [inputView setTextInputState:@{
1553 @"text" : @"SELECTION",
1554 @"selectionBase" : @-1,
1555 @"selectionExtent" : @-1
1557 [inputView updateEditingState];
1558 OCMVerify([
engine flutterTextInputView:inputView
1559 updateEditingClient:0
1560 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1561 return ([state[
@"selectionBase"] intValue]) == 0 &&
1562 ([state[@"selectionExtent"] intValue] == 0);
1567 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1568 [inputView updateEditingState];
1569 OCMVerify([
engine flutterTextInputView:inputView
1570 updateEditingClient:0
1571 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1572 return ([state[
@"selectionBase"] intValue]) == 0 &&
1573 ([state[@"selectionExtent"] intValue] == 0);
1577 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1578 [inputView updateEditingState];
1579 OCMVerify([
engine flutterTextInputView:inputView
1580 updateEditingClient:0
1581 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1582 return ([state[
@"selectionBase"] intValue]) == 0 &&
1583 ([state[@"selectionExtent"] intValue] == 0);
1587- (void)testUpdateEditingClientSelectionClamping {
1591 [inputView.text setString:@"SELECTION"];
1592 inputView.markedTextRange = nil;
1593 inputView.selectedTextRange = nil;
1596 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1597 [inputView updateEditingState];
1598 OCMVerify([
engine flutterTextInputView:inputView
1599 updateEditingClient:0
1600 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1601 return ([state[
@"selectionBase"] intValue]) == 0 &&
1602 ([state[@"selectionExtent"] intValue] == 0);
1606 [inputView setTextInputState:@{
1607 @"text" : @"SELECTION",
1608 @"selectionBase" : @0,
1609 @"selectionExtent" : @9999
1611 [inputView updateEditingState];
1613 OCMVerify([
engine flutterTextInputView:inputView
1614 updateEditingClient:0
1615 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1616 return ([state[
@"selectionBase"] intValue]) == 0 &&
1617 ([state[@"selectionExtent"] intValue] == 9);
1622 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1623 [inputView updateEditingState];
1624 OCMVerify([
engine flutterTextInputView:inputView
1625 updateEditingClient:0
1626 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1627 return ([state[
@"selectionBase"] intValue]) == 0 &&
1628 ([state[@"selectionExtent"] intValue] == 1);
1632 [inputView setTextInputState:@{
1633 @"text" : @"SELECTION",
1634 @"selectionBase" : @9999,
1635 @"selectionExtent" : @9999
1637 [inputView updateEditingState];
1638 OCMVerify([
engine flutterTextInputView:inputView
1639 updateEditingClient:0
1640 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1641 return ([state[
@"selectionBase"] intValue]) == 9 &&
1642 ([state[@"selectionExtent"] intValue] == 9);
1646- (void)testInputViewsHasNonNilInputDelegate {
1648 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1650 [inputView setTextInputClient:123];
1651 [inputView reloadInputViews];
1652 [inputView becomeFirstResponder];
1653 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1654 inputView.inputDelegate = nil;
1658 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1659 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1660 [inputView removeFromSuperview];
1663- (void)testInputViewsDoNotHaveUITextInteractions {
1665 BOOL hasTextInteraction = NO;
1666 for (
id interaction in inputView.interactions) {
1667 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1668 if (hasTextInteraction) {
1672 XCTAssertFalse(hasTextInteraction);
1675#pragma mark - UITextInput methods - Tests
1677- (void)testUpdateFirstRectForRange {
1678 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1684 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1689 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1690 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1694 NSArray* affineMatrix = @[
1695 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1696 @(-6.0), @(3.0), @(9.0), @(1.0)
1700 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1702 [inputView setEditableTransform:yOffsetMatrix];
1704 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1707 CGRect testRect = CGRectMake(0, 0, 100, 100);
1708 [inputView setMarkedRect:testRect];
1710 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1711 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1713 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1716 [inputView setEditableTransform:zeroMatrix];
1718 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1719 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1722 [inputView setEditableTransform:yOffsetMatrix];
1723 [inputView setMarkedRect:testRect];
1724 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1727 [inputView setMarkedRect:kInvalidFirstRect];
1729 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1730 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1733 [inputView setEditableTransform:affineMatrix];
1734 [inputView setMarkedRect:testRect];
1736 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1738 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1739 const CGPoint offset = CGPointMake(113, 119);
1740 CGRect currentFrame = inputView.frame;
1741 currentFrame.origin = offset;
1742 inputView.frame = currentFrame;
1745 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1746 [inputView firstRectForRange:range]));
1749- (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1751 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1756 [inputView setSelectionRects:@[
1765 if (@available(iOS 17, *)) {
1766 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1767 [inputView firstRectForRange:multiRectRange]));
1769 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1770 [inputView firstRectForRange:multiRectRange]));
1774- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1776 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1778 [inputView setSelectionRects:@[
1785 if (@available(iOS 17, *)) {
1786 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1787 [inputView firstRectForRange:singleRectRange]));
1789 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1794 if (@available(iOS 17, *)) {
1795 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1796 [inputView firstRectForRange:multiRectRange]));
1798 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1801 [inputView setTextInputState:@{@"text" : @"COM"}];
1803 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1806- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1808 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1810 [inputView setSelectionRects:@[
1817 if (@available(iOS 17, *)) {
1818 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1819 [inputView firstRectForRange:singleRectRange]));
1821 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1825 if (@available(iOS 17, *)) {
1826 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1827 [inputView firstRectForRange:multiRectRange]));
1829 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1832 [inputView setTextInputState:@{@"text" : @"COM"}];
1834 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1837- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1839 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1841 [inputView setSelectionRects:@[
1852 if (@available(iOS 17, *)) {
1853 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1854 [inputView firstRectForRange:singleRectRange]));
1856 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1861 if (@available(iOS 17, *)) {
1862 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1863 [inputView firstRectForRange:multiRectRange]));
1865 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1869- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1871 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1873 [inputView setSelectionRects:@[
1884 if (@available(iOS 17, *)) {
1885 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1886 [inputView firstRectForRange:singleRectRange]));
1888 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1892 if (@available(iOS 17, *)) {
1893 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1894 [inputView firstRectForRange:multiRectRange]));
1896 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1900- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1902 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1904 [inputView setSelectionRects:@[
1915 if (@available(iOS 17, *)) {
1916 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1917 [inputView firstRectForRange:multiRectRange]));
1919 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1923- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1925 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1927 [inputView setSelectionRects:@[
1938 if (@available(iOS 17, *)) {
1939 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1940 [inputView firstRectForRange:multiRectRange]));
1942 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1946- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1948 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1950 [inputView setSelectionRects:@[
1961 if (@available(iOS 17, *)) {
1962 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1963 [inputView firstRectForRange:multiRectRange]));
1965 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1969- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1971 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1973 [inputView setSelectionRects:@[
1984 if (@available(iOS 17, *)) {
1985 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1986 [inputView firstRectForRange:multiRectRange]));
1988 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1992- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1994 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1996 [inputView setSelectionRects:@[
2007 if (@available(iOS 17, *)) {
2008 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
2009 [inputView firstRectForRange:multiRectRange]));
2011 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2015- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
2017 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2019 [inputView setSelectionRects:@[
2030 if (@available(iOS 17, *)) {
2031 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
2032 [inputView firstRectForRange:multiRectRange]));
2034 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2038- (void)testClosestPositionToPoint {
2040 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2043 [inputView setSelectionRects:@[
2048 CGPoint point = CGPointMake(150, 150);
2049 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2050 XCTAssertEqual(UITextStorageDirectionBackward,
2055 [inputView setSelectionRects:@[
2062 point = CGPointMake(125, 150);
2063 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2064 XCTAssertEqual(UITextStorageDirectionForward,
2069 [inputView setSelectionRects:@[
2076 point = CGPointMake(125, 201);
2077 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2078 XCTAssertEqual(UITextStorageDirectionBackward,
2082 [inputView setSelectionRects:@[
2088 point = CGPointMake(125, 250);
2089 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2090 XCTAssertEqual(UITextStorageDirectionBackward,
2094 [inputView setSelectionRects:@[
2099 point = CGPointMake(110, 50);
2100 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2101 XCTAssertEqual(UITextStorageDirectionForward,
2106 [inputView beginFloatingCursorAtPoint:CGPointZero];
2107 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2108 XCTAssertEqual(UITextStorageDirectionForward,
2110 [inputView endFloatingCursor];
2113- (void)testClosestPositionToPointRTL {
2115 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2117 [inputView setSelectionRects:@[
2133 XCTAssertEqual(0U, position.
index);
2134 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2136 XCTAssertEqual(1U, position.
index);
2137 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2139 XCTAssertEqual(1U, position.
index);
2140 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2142 XCTAssertEqual(2U, position.
index);
2143 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2145 XCTAssertEqual(2U, position.
index);
2146 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2148 XCTAssertEqual(3U, position.
index);
2149 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2151 XCTAssertEqual(3U, position.
index);
2152 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2155- (void)testSelectionRectsForRange {
2157 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2159 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2160 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2161 [inputView setSelectionRects:@[
2170 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2171 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2172 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2176 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2177 XCTAssertTrue(CGRectEqualToRect(
2178 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2179 [inputView selectionRectsForRange:range][0].rect));
2182- (void)testClosestPositionToPointWithinRange {
2184 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2187 [inputView setSelectionRects:@[
2194 CGPoint point = CGPointMake(125, 150);
2197 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2199 UITextStorageDirectionForward,
2200 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2203 [inputView setSelectionRects:@[
2210 point = CGPointMake(125, 150);
2213 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2215 UITextStorageDirectionForward,
2216 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2219- (void)testClosestPositionToPointWithPartialSelectionRects {
2221 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2228 XCTAssertTrue(CGRectEqualToRect(
2231 affinity:UITextStorageDirectionForward]],
2232 CGRectMake(100, 0, 0, 100)));
2235 XCTAssertTrue(CGRectEqualToRect(
2238 affinity:UITextStorageDirectionForward]],
2242#pragma mark - Floating Cursor - Tests
2244- (void)testFloatingCursorDoesNotThrow {
2247 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2248 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2249 [inputView endFloatingCursor];
2250 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2251 [inputView endFloatingCursor];
2254- (void)testFloatingCursor {
2256 [inputView setTextInputState:@{
2258 @"selectionBase" : @1,
2259 @"selectionExtent" : @1,
2270 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2273 XCTAssertTrue(CGRectEqualToRect(
2276 affinity:UITextStorageDirectionForward]],
2277 CGRectMake(0, 0, 0, 100)));
2280 XCTAssertTrue(CGRectEqualToRect(
2283 affinity:UITextStorageDirectionForward]],
2284 CGRectMake(100, 100, 0, 100)));
2285 XCTAssertTrue(CGRectEqualToRect(
2288 affinity:UITextStorageDirectionForward]],
2289 CGRectMake(200, 200, 0, 100)));
2290 XCTAssertTrue(CGRectEqualToRect(
2293 affinity:UITextStorageDirectionForward]],
2294 CGRectMake(300, 300, 0, 100)));
2297 XCTAssertTrue(CGRectEqualToRect(
2300 affinity:UITextStorageDirectionForward]],
2301 CGRectMake(400, 300, 0, 100)));
2303 XCTAssertTrue(CGRectEqualToRect(
2306 affinity:UITextStorageDirectionForward]],
2310 [inputView setTextInputState:@{
2312 @"selectionBase" : @2,
2313 @"selectionExtent" : @2,
2316 XCTAssertTrue(CGRectEqualToRect(
2319 affinity:UITextStorageDirectionBackward]],
2320 CGRectMake(0, 0, 0, 100)));
2323 XCTAssertTrue(CGRectEqualToRect(
2326 affinity:UITextStorageDirectionBackward]],
2327 CGRectMake(100, 0, 0, 100)));
2328 XCTAssertTrue(CGRectEqualToRect(
2331 affinity:UITextStorageDirectionBackward]],
2332 CGRectMake(200, 100, 0, 100)));
2333 XCTAssertTrue(CGRectEqualToRect(
2336 affinity:UITextStorageDirectionBackward]],
2337 CGRectMake(300, 200, 0, 100)));
2338 XCTAssertTrue(CGRectEqualToRect(
2341 affinity:UITextStorageDirectionBackward]],
2342 CGRectMake(400, 300, 0, 100)));
2344 XCTAssertTrue(CGRectEqualToRect(
2347 affinity:UITextStorageDirectionBackward]],
2352 CGRect initialBounds = inputView.bounds;
2353 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2354 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2355 OCMVerify([
engine flutterTextInputView:inputView
2356 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2358 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2359 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2360 ([state[
@"Y"] isEqualToNumber:@(0)]);
2363 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2364 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2365 OCMVerify([
engine flutterTextInputView:inputView
2366 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2368 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2369 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2370 ([state[
@"Y"] isEqualToNumber:@(333)]);
2373 [inputView endFloatingCursor];
2374 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2375 OCMVerify([
engine flutterTextInputView:inputView
2376 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2378 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2379 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2380 ([state[
@"Y"] isEqualToNumber:@(0)]);
2384#pragma mark - UIKeyInput Overrides - Tests
2386- (void)testInsertTextAddsPlaceholderSelectionRects {
2389 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2399 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2402 [inputView insertText:@"in"];
2430#pragma mark - Autofill - Utilities
2432- (NSMutableDictionary*)mutablePasswordTemplateCopy {
2435 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2436 @"keyboardAppearance" :
@"Brightness.light",
2437 @"obscureText" : @YES,
2438 @"inputAction" :
@"TextInputAction.unspecified",
2439 @"smartDashesType" :
@"0",
2440 @"smartQuotesType" :
@"0",
2441 @"autocorrect" : @YES
2445 return [_passwordTemplate mutableCopy];
2449 return [
self.installedInputViews
2450 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2453- (void)commitAutofillContextAndVerify {
2457 [textInputPlugin handleMethodCall:methodCall
2458 result:^(id _Nullable result){
2461 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2466 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2470#pragma mark - Autofill - Tests
2472- (void)testDisablingAutofillOnInputClient {
2473 NSDictionary* config =
self.mutableTemplateCopy;
2474 [config setValue:@"YES" forKey:@"obscureText"];
2476 [
self setClientId:123 configuration:config];
2479 XCTAssertEqualObjects(inputView.textContentType,
@"");
2482- (void)testAutofillEnabledByDefault {
2483 NSDictionary* config =
self.mutableTemplateCopy;
2484 [config setValue:@"NO" forKey:@"obscureText"];
2485 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2486 forKey:@"autofill"];
2488 [
self setClientId:123 configuration:config];
2491 XCTAssertNil(inputView.textContentType);
2494- (void)testAutofillContext {
2495 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2498 @"uniqueIdentifier" : @"field1",
2499 @"hints" : @[ @"hint1" ],
2500 @"editingValue" : @{@"text" : @""}
2502 forKey:@"autofill"];
2504 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2506 @"uniqueIdentifier" : @"field2",
2507 @"hints" : @[ @"hint2" ],
2508 @"editingValue" : @{@"text" : @""}
2510 forKey:@"autofill"];
2512 NSMutableDictionary* config = [field1 mutableCopy];
2513 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2515 [
self setClientId:123 configuration:config];
2516 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2520 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2521 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2523 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2526 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2528 @"uniqueIdentifier" : @"field3",
2529 @"hints" : @[ @"hint3" ],
2530 @"editingValue" : @{@"text" : @""}
2532 forKey:@"autofill"];
2536 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2538 [
self setClientId:123 configuration:config];
2540 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2543 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2544 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2546 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2549 for (NSString*
key in oldContext.allKeys) {
2554 config =
self.mutablePasswordTemplateCopy;
2557 [
self setClientId:124 configuration:config];
2558 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2560 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2563 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2564 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2567 for (NSString*
key in oldContext.allKeys) {
2572 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2576 [
self setClientId:200 configuration:config];
2579 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2582 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2583 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2586 for (NSString*
key in oldContext.allKeys) {
2590 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2593- (void)testCommitAutofillContext {
2594 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2596 @"uniqueIdentifier" : @"field1",
2597 @"hints" : @[ @"hint1" ],
2598 @"editingValue" : @{@"text" : @""}
2600 forKey:@"autofill"];
2602 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2604 @"uniqueIdentifier" : @"field2",
2605 @"hints" : @[ @"hint2" ],
2606 @"editingValue" : @{@"text" : @""}
2608 forKey:@"autofill"];
2610 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2612 @"uniqueIdentifier" : @"field3",
2613 @"hints" : @[ @"hint3" ],
2614 @"editingValue" : @{@"text" : @""}
2616 forKey:@"autofill"];
2618 NSMutableDictionary* config = [field1 mutableCopy];
2619 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2621 [
self setClientId:123 configuration:config];
2622 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2624 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2626 [
self commitAutofillContextAndVerify];
2627 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2630 [
self setClientId:123 configuration:config];
2632 [
self setClientId:124 configuration:field3];
2633 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2635 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2636 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2639 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2641 [
self commitAutofillContextAndVerify];
2642 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2645 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2647 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2651 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2652 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2654 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2656 [
self commitAutofillContextAndVerify];
2657 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2660- (void)testAutofillInputViews {
2661 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2663 @"uniqueIdentifier" : @"field1",
2664 @"hints" : @[ @"hint1" ],
2665 @"editingValue" : @{@"text" : @""}
2667 forKey:@"autofill"];
2669 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2671 @"uniqueIdentifier" : @"field2",
2672 @"hints" : @[ @"hint2" ],
2673 @"editingValue" : @{@"text" : @""}
2675 forKey:@"autofill"];
2677 NSMutableDictionary* config = [field1 mutableCopy];
2678 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2680 [
self setClientId:123 configuration:config];
2681 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2684 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2687 XCTAssertEqual(inputFields.count, 2ul);
2688 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2693 withText:@"Autofilled!"];
2694 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2697 OCMVerify([
engine flutterTextInputView:inactiveView
2698 updateEditingClient:0
2699 withState:[OCMArg isNotNil]
2700 withTag:
@"field2"]);
2703- (void)testAutofillContextPersistsAfterClearClient {
2704 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2706 @"uniqueIdentifier" : @"field1",
2707 @"hints" : @[ @"username" ],
2708 @"editingValue" : @{@"text" : @""}
2710 forKey:@"autofill"];
2712 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2714 @"uniqueIdentifier" : @"field2",
2715 @"hints" : @[ @"password" ],
2716 @"editingValue" : @{@"text" : @""}
2718 forKey:@"autofill"];
2720 NSMutableDictionary* config = [field1 mutableCopy];
2721 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2724 [
self setClientId:123 configuration:config];
2729 [
self setClientClear];
2734 [
self commitAutofillContextAndVerify];
2739- (void)testPasswordAutofillHack {
2740 NSDictionary* config =
self.mutableTemplateCopy;
2741 [config setValue:@"YES" forKey:@"obscureText"];
2742 [
self setClientId:123 configuration:config];
2745 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2749 XCTAssert([inputView isKindOfClass:[UITextField class]]);
2752 XCTAssertNotEqual([inputView performSelector:@selector(font)], nil);
2755- (void)testClearAutofillContextClearsSelection {
2756 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2757 NSDictionary* editingValue = @{
2758 @"text" :
@"REGULAR_TEXT_FIELD",
2759 @"composingBase" : @0,
2760 @"composingExtent" : @3,
2761 @"selectionBase" : @1,
2762 @"selectionExtent" : @4
2764 [regularField setValue:@{
2765 @"uniqueIdentifier" : @"field2",
2766 @"hints" : @[ @"hint2" ],
2767 @"editingValue" : editingValue,
2769 forKey:@"autofill"];
2770 [regularField addEntriesFromDictionary:editingValue];
2771 [
self setClientId:123 configuration:regularField];
2772 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2773 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2776 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2778 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(1, 3)));
2782 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2783 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2785 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2787 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2788 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2791 XCTAssert([oldInputView.text isEqualToString:
@""]);
2793 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(0, 0)));
2796- (void)testGarbageInputViewsAreNotRemovedImmediately {
2798 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2799 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2801 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2804 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2805 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2807 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2809 [
self commitAutofillContextAndVerify];
2812- (void)testScribbleSetSelectionRects {
2813 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2814 NSDictionary* editingValue = @{
2815 @"text" :
@"REGULAR_TEXT_FIELD",
2816 @"composingBase" : @0,
2817 @"composingExtent" : @3,
2818 @"selectionBase" : @1,
2819 @"selectionExtent" : @4
2821 [regularField setValue:@{
2822 @"uniqueIdentifier" : @"field1",
2823 @"hints" : @[ @"hint2" ],
2824 @"editingValue" : editingValue,
2826 forKey:@"autofill"];
2827 [regularField addEntriesFromDictionary:editingValue];
2828 [
self setClientId:123 configuration:regularField];
2829 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2830 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2832 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2833 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2837 [textInputPlugin handleMethodCall:methodCall
2838 result:^(id _Nullable result){
2841 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2844- (void)testDecommissionedViewAreNotReusedByAutofill {
2846 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2847 [configuration setValue:@{
2848 @"uniqueIdentifier" : @"field1",
2849 @"hints" : @[ UITextContentTypePassword ],
2850 @"editingValue" : @{@"text" : @""}
2852 forKey:@"autofill"];
2853 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2855 [
self setClientId:123 configuration:configuration];
2857 [
self setTextInputHide];
2860 [
self setClientId:124 configuration:configuration];
2864 XCTAssertNotNil(previousActiveView);
2868- (void)testInitialActiveViewCantAccessTextInputDelegate {
2875- (void)testAutoFillDoesNotTriggerOnShowAndHideKeyboard {
2877 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2878 [configuration setValue:@{
2879 @"uniqueIdentifier" : @"field1",
2880 @"hints" : @[ UITextContentTypePassword ],
2881 @"editingValue" : @{@"text" : @""}
2883 forKey:@"autofill"];
2884 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2885 [
self setClientId:123 configuration:configuration];
2887 [
self setTextInputShow];
2888 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2891 [
self setTextInputHide];
2892 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2894 [
self commitAutofillContextAndVerify];
2897#pragma mark - Accessibility - Tests
2899- (void)testUITextInputAccessibilityNotHiddenWhenKeyboardIsShownAndHidden {
2900 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2903 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2906 XCTAssertEqual([inputFields count], 1u);
2909 [
self setTextInputShow];
2911 inputFields =
self.installedInputViews;
2913 XCTAssertEqual([inputFields count], 1u);
2916 [
self setTextInputHide];
2918 inputFields =
self.installedInputViews;
2920 XCTAssertEqual([inputFields count], 1u);
2923 [
self setClientClear];
2925 inputFields =
self.installedInputViews;
2928 XCTAssertEqual([inputFields count], 0u);
2931- (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2934 UIView* container = [[UIView alloc] init];
2935 UIAccessibilityElement* backing =
2936 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2937 inputView.backingTextInputAccessibilityObject = backing;
2939 inputView.isAccessibilityFocused = YES;
2940 [inputView accessibilityElementDidBecomeFocused];
2942 XCTAssertEqual(inputView.receivedNotification, UIAccessibilityScreenChangedNotification);
2943 XCTAssertEqual(inputView.receivedNotificationTarget, backing);
2946- (void)testFlutterTokenizerCanParseLines {
2948 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2951 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2952 XCTAssertEqual(range.
range.location, 0u);
2953 XCTAssertEqual(range.
range.length, 0u);
2955 [inputView insertText:@"how are you\nI am fine, Thank you"];
2957 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2958 XCTAssertEqual(range.
range.location, 0u);
2959 XCTAssertEqual(range.
range.length, 11u);
2961 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2962 XCTAssertEqual(range.
range.location, 0u);
2963 XCTAssertEqual(range.
range.length, 11u);
2965 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2966 XCTAssertEqual(range.
range.location, 0u);
2967 XCTAssertEqual(range.
range.length, 11u);
2969 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2970 XCTAssertEqual(range.
range.location, 12u);
2971 XCTAssertEqual(range.
range.length, 20u);
2973 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2974 XCTAssertEqual(range.
range.location, 12u);
2975 XCTAssertEqual(range.
range.length, 20u);
2977 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2978 XCTAssertEqual(range.
range.location, 12u);
2979 XCTAssertEqual(range.
range.length, 20u);
2982- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2984 [inputView insertText:@"0123456789\n012345"];
2985 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2988 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2989 withGranularity:UITextGranularityLine
2990 inDirection:UITextStorageDirectionBackward];
2991 XCTAssertEqual(range.
range.location, 11u);
2992 XCTAssertEqual(range.
range.length, 6u);
2995- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2997 [inputView insertText:@"0123456789\n012345"];
2998 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3001 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3002 withGranularity:UITextGranularityLine
3003 inDirection:UITextStorageDirectionForward];
3004 if (@available(iOS 17.0, *)) {
3005 XCTAssertNil(range);
3007 XCTAssertEqual(range.
range.location, 11u);
3008 XCTAssertEqual(range.
range.length, 6u);
3012- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
3014 [inputView insertText:@"0123456789\n012345"];
3015 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3020 withGranularity:UITextGranularityLine
3021 inDirection:UITextStorageDirectionForward];
3022 if (@available(iOS 17.0, *)) {
3023 XCTAssertNil(range);
3025 XCTAssertEqual(range.
range.location, 0u);
3026 XCTAssertEqual(range.
range.length, 0u);
3030- (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
3035 __weak UIView* activeView;
3040 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
3043 result:^(id _Nullable result){
3049 result:^(id _Nullable result){
3051 XCTAssertNotNil(activeView);
3054 XCTAssertNotNil(activeView);
3057- (void)testFlutterTextInputPluginHostViewNilCrash {
3060 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
3063- (void)testFlutterTextInputPluginHostViewNotNil {
3069 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
3072- (void)testSetPlatformViewClient {
3079 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
3081 result:^(id _Nullable result){
3084 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
3087 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
3089 result:^(id _Nullable result){
3091 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
3094- (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
3095 if (@available(iOS 16.0, *)) {
3097 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3098 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
3099 @"editMenuInteraction setup delegate correctly");
3103- (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
3104 if (@available(iOS 16.0, *)) {
3108 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
3112- (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
3113 if (@available(iOS 16.0, *)) {
3118 [myViewController loadView];
3121 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3123 result:^(id _Nullable result){
3129 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3131 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3132 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3134 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3135 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3136 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3137 .andDo(^(NSInvocation* invocation) {
3139 [invocation retainArguments];
3140 UIEditMenuConfiguration* config;
3141 [invocation getArgument:&config atIndex:2];
3142 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
3143 @"UIEditMenuConfiguration must use automatic arrow direction.");
3144 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
3145 @"UIEditMenuConfiguration must have the correct point.");
3146 [expectation fulfill];
3149 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3150 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
3152 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3153 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3154 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3158- (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
3159 if (@available(iOS 16.0, *)) {
3164 [myViewController loadView];
3168 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3170 result:^(id _Nullable result){
3176 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3178 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3179 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3181 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3182 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3183 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3184 .andDo(^(NSInvocation* invocation) {
3185 [expectation fulfill];
3188 myInputView.frame = CGRectMake(10, 20, 30, 40);
3189 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3190 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3192 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3193 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3194 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3197 [myInputView editMenuInteraction:mockInteraction
3198 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3200 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3201 @"targetRectForConfiguration must return the correct target rect.");
3205- (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3206 if (@available(iOS 16.0, *)) {
3211 [myViewController loadView];
3215 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3217 result:^(id _Nullable result){
3223 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3225 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3226 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3228 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3229 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3230 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3231 .andDo(^(NSInvocation* invocation) {
3232 [expectation fulfill];
3235 myInputView.frame = CGRectMake(10, 20, 30, 40);
3236 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3237 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3239 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3240 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3241 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3243 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3245 action:@selector(copy:)
3247 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3249 action:@selector(paste:)
3251 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3253 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3254 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3255 suggestedActions:suggestedActions];
3256 XCTAssertEqualObjects(menu.children, suggestedActions,
3257 @"Must show suggested items by default.");
3261- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3262 if (@available(iOS 16.0, *)) {
3267 [myViewController loadView];
3271 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3273 result:^(id _Nullable result){
3279 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3281 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3282 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3284 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3285 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3286 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3287 .andDo(^(NSInvocation* invocation) {
3288 [expectation fulfill];
3291 myInputView.frame = CGRectMake(10, 20, 30, 40);
3292 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3293 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3295 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3296 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3298 BOOL shownEditMenu =
3299 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3300 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3301 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3303 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3305 action:@selector(copy:)
3307 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3309 action:@selector(paste:)
3311 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3313 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3314 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3315 suggestedActions:suggestedActions];
3317 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3318 XCTAssertEqualObjects(menu.children, expectedChildren);
3322- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3323 if (@available(iOS 16.0, *)) {
3328 [myViewController loadView];
3332 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3334 result:^(id _Nullable result){
3340 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3342 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3343 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3345 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3346 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3347 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3348 .andDo(^(NSInvocation* invocation) {
3349 [expectation fulfill];
3352 myInputView.frame = CGRectMake(10, 20, 30, 40);
3353 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3354 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3356 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3357 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3359 BOOL shownEditMenu =
3360 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3361 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3362 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3364 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3366 action:@selector(copy:)
3368 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3370 action:@selector(cut:)
3372 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3374 action:@selector(paste:)
3387 NSArray<UIMenuElement*>* suggestedActions = @[
3388 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3389 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3392 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3393 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3394 suggestedActions:suggestedActions];
3396 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3397 XCTAssertEqualObjects(menu.children, expectedActions);
3401- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3402 if (@available(iOS 16.0, *)) {
3407 [myViewController loadView];
3411 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3413 result:^(id _Nullable result){
3419 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3421 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3422 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3424 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3425 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3426 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3427 .andDo(^(NSInvocation* invocation) {
3428 [expectation fulfill];
3431 myInputView.frame = CGRectMake(10, 20, 30, 40);
3432 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3433 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3435 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3436 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3437 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3440 BOOL shownEditMenu =
3441 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3442 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3443 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3445 NSArray<UICommand*>* suggestedActions = @[
3446 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3449 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3450 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3451 suggestedActions:suggestedActions];
3452 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3454 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3455 @"Must create search web item in the tree.");
3456 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3457 @"Must create look up item in the tree.");
3458 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3459 @"Must create share item in the tree.");
3463- (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3465 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3467 [inputView setTextInputClient:123];
3468 [inputView reloadInputViews];
3469 [inputView becomeFirstResponder];
3470 XCTAssert(inputView.isFirstResponder);
3472 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3473 [NSNotificationCenter.defaultCenter
3474 postNotificationName:UIKeyboardWillShowNotification
3476 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3480 [textInputPlugin handleMethodCall:onPointerMoveCall
3481 result:^(id _Nullable result){
3483 XCTAssertFalse(inputView.isFirstResponder);
3487- (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3488 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3489 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3490 UIScene* scene = scenes.anyObject;
3491 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3492 UIWindowScene* windowScene = (UIWindowScene*)scene;
3493 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3494 UIWindow*
window = windowScene.windows[0];
3495 [window addSubview:viewController.view];
3497 [viewController loadView];
3500 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3502 [inputView setTextInputClient:123];
3503 [inputView reloadInputViews];
3504 [inputView becomeFirstResponder];
3507 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3508 [subView removeFromSuperview];
3512 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3513 [NSNotificationCenter.defaultCenter
3514 postNotificationName:UIKeyboardWillShowNotification
3516 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3520 [textInputPlugin handleMethodCall:onPointerMoveCall
3521 result:^(id _Nullable result){
3524 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3525 [subView removeFromSuperview];
3530- (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3531 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3532 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3533 UIScene* scene = scenes.anyObject;
3534 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3535 UIWindowScene* windowScene = (UIWindowScene*)scene;
3536 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3537 UIWindow*
window = windowScene.windows[0];
3538 [window addSubview:viewController.view];
3540 [viewController loadView];
3543 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3545 [inputView setTextInputClient:123];
3546 [inputView reloadInputViews];
3547 [inputView becomeFirstResponder];
3549 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3550 [NSNotificationCenter.defaultCenter
3551 postNotificationName:UIKeyboardWillShowNotification
3553 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3557 [textInputPlugin handleMethodCall:onPointerMoveCall
3558 result:^(id _Nullable result){
3562 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3567 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3568 result:^(id _Nullable result){
3572 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3574 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3575 [subView removeFromSuperview];
3580- (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3581 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3582 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3583 UIScene* scene = scenes.anyObject;
3584 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3585 UIWindowScene* windowScene = (UIWindowScene*)scene;
3586 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3587 UIWindow*
window = windowScene.windows[0];
3588 [window addSubview:viewController.view];
3590 [viewController loadView];
3593 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3595 [inputView setTextInputClient:123];
3596 [inputView reloadInputViews];
3597 [inputView becomeFirstResponder];
3599 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3600 [NSNotificationCenter.defaultCenter
3601 postNotificationName:UIKeyboardWillShowNotification
3603 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3607 [textInputPlugin handleMethodCall:onPointerMoveCall
3608 result:^(id _Nullable result){
3611 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3616 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3617 result:^(id _Nullable result){
3620 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3625 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3626 result:^(id _Nullable result){
3629 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3630 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3631 [subView removeFromSuperview];
3636- (void)testInteractiveKeyboardFindFirstResponderRecursive {
3638 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3639 [inputView setTextInputClient:123];
3640 [inputView reloadInputViews];
3641 [inputView becomeFirstResponder];
3643 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3644 XCTAssertEqualObjects(inputView, firstResponder);
3648- (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3655 [subInputView addSubview:subFirstResponderInputView];
3656 [inputView addSubview:subInputView];
3657 [inputView addSubview:otherSubInputView];
3658 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3659 [inputView setTextInputClient:123];
3660 [inputView reloadInputViews];
3661 [subInputView setTextInputClient:123];
3662 [subInputView reloadInputViews];
3663 [otherSubInputView setTextInputClient:123];
3664 [otherSubInputView reloadInputViews];
3665 [subFirstResponderInputView setTextInputClient:123];
3666 [subFirstResponderInputView reloadInputViews];
3667 [subFirstResponderInputView becomeFirstResponder];
3669 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3670 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3674- (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3676 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3677 [inputView setTextInputClient:123];
3678 [inputView reloadInputViews];
3680 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3681 XCTAssertNil(firstResponder);
3685- (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3686 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3687 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3688 UIScene* scene = scenes.anyObject;
3689 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3690 UIWindowScene* windowScene = (UIWindowScene*)scene;
3691 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3692 UIWindow*
window = windowScene.windows[0];
3693 [window addSubview:viewController.view];
3695 [viewController loadView];
3697 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3698 initWithDescription:
3699 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3700 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3701 .andDo(^(NSInvocation* invocation) {
3702 [expectation fulfill];
3704 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3705 [NSNotificationCenter.defaultCenter
3706 postNotificationName:UIKeyboardWillShowNotification
3708 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3712 [textInputPlugin handleMethodCall:initialMoveCall
3713 result:^(id _Nullable result){
3718 [textInputPlugin handleMethodCall:subsequentMoveCall
3719 result:^(id _Nullable result){
3725 [textInputPlugin handleMethodCall:pointerUpCall
3726 result:^(id _Nullable result){
3729 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3733- (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3734 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3735 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3736 UIScene* scene = scenes.anyObject;
3737 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3738 UIWindowScene* windowScene = (UIWindowScene*)scene;
3739 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3740 UIWindow*
window = windowScene.windows[0];
3741 [window addSubview:viewController.view];
3743 [viewController loadView];
3745 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3746 [NSNotificationCenter.defaultCenter
3747 postNotificationName:UIKeyboardWillShowNotification
3749 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3753 [textInputPlugin handleMethodCall:initialMoveCall
3754 result:^(id _Nullable result){
3759 [textInputPlugin handleMethodCall:subsequentMoveCall
3760 result:^(id _Nullable result){
3766 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3767 result:^(id _Nullable result){
3773 [textInputPlugin handleMethodCall:pointerUpCall
3774 result:^(id _Nullable result){
3776 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3777 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3779 XCTNSPredicateExpectation* expectation =
3780 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3781 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3785- (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3786 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3787 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3788 UIScene* scene = scenes.anyObject;
3789 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3790 UIWindowScene* windowScene = (UIWindowScene*)scene;
3791 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3792 UIWindow*
window = windowScene.windows[0];
3793 [window addSubview:viewController.view];
3795 [viewController loadView];
3798 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3800 [inputView setTextInputClient:123];
3801 [inputView reloadInputViews];
3802 [inputView becomeFirstResponder];
3804 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3805 [NSNotificationCenter.defaultCenter
3806 postNotificationName:UIKeyboardWillShowNotification
3808 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3812 [textInputPlugin handleMethodCall:initialMoveCall
3813 result:^(id _Nullable result){
3818 [textInputPlugin handleMethodCall:subsequentMoveCall
3819 result:^(id _Nullable result){
3825 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3826 result:^(id _Nullable result){
3832 [textInputPlugin handleMethodCall:pointerUpCall
3833 result:^(id _Nullable result){
3835 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3836 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3838 XCTNSPredicateExpectation* expectation =
3839 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3840 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3844- (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3845 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3846 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3847 UIScene* scene = scenes.anyObject;
3848 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3849 UIWindowScene* windowScene = (UIWindowScene*)scene;
3850 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3851 UIWindow*
window = windowScene.windows[0];
3852 [window addSubview:viewController.view];
3854 [viewController loadView];
3856 XCTestExpectation* expectation =
3857 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3858 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3859 [NSNotificationCenter.defaultCenter
3860 postNotificationName:UIKeyboardWillShowNotification
3862 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3866 [textInputPlugin handleMethodCall:initialMoveCall
3867 result:^(id _Nullable result){
3872 [textInputPlugin handleMethodCall:subsequentMoveCall
3873 result:^(id _Nullable result){
3878 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3879 result:^(id _Nullable result){
3886 handleMethodCall:pointerUpCall
3887 result:^(id _Nullable result) {
3888 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3889 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3890 keyboardFrame.origin.y);
3891 [expectation fulfill];
3896- (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3897 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3898 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3899 UIScene* scene = scenes.anyObject;
3900 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3901 UIWindowScene* windowScene = (UIWindowScene*)scene;
3902 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3903 UIWindow*
window = windowScene.windows[0];
3904 [window addSubview:viewController.view];
3906 [viewController loadView];
3908 XCTestExpectation* expectation =
3909 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3910 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3911 [NSNotificationCenter.defaultCenter
3912 postNotificationName:UIKeyboardWillShowNotification
3914 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3918 [textInputPlugin handleMethodCall:initialMoveCall
3919 result:^(id _Nullable result){
3924 [textInputPlugin handleMethodCall:subsequentMoveCall
3925 result:^(id _Nullable result){
3932 handleMethodCall:pointerUpCall
3933 result:^(id _Nullable result) {
3934 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3935 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3936 [expectation fulfill];
3940- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3941 [UIView setAnimationsEnabled:YES];
3942 [textInputPlugin showKeyboardAndRemoveScreenshot];
3944 UIView.areAnimationsEnabled,
3945 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3948- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3949 [UIView setAnimationsEnabled:YES];
3950 [textInputPlugin showKeyboardAndRemoveScreenshot];
3952 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3954 return UIView.areAnimationsEnabled;
3956 XCTNSPredicateExpectation* expectation =
3957 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3958 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3961- (void)testEditMenu_shouldCreateCustomMenuItemWithCorrectProperties {
3962 if (@available(iOS 16.0, *)) {
3967 [myViewController loadView];
3971 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3973 result:^(id _Nullable result){
3978 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3980 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3981 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3983 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3984 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
3986 NSArray<NSDictionary*>* encodedItems = @[
3987 @{@"type" : @"custom", @"id" : @"custom-action-1", @"title" : @"Custom Action 1"},
3988 @{@"type" : @"custom", @"id" : @"custom-action-2", @"title" : @"Custom Action 2"},
3991 BOOL shownEditMenu =
3992 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3993 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
3995 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3996 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3997 suggestedActions:@[]];
3999 XCTAssertEqual(menu.children.count, 2UL,
@"Should create 2 custom menu items");
4000 UIAction* firstAction = (UIAction*)menu.children[0];
4001 UIAction* secondAction = (UIAction*)menu.children[1];
4002 XCTAssertEqualObjects(firstAction.title,
@"Custom Action 1",
4003 @"First action title should match");
4004 XCTAssertEqualObjects(secondAction.title,
@"Custom Action 2",
4005 @"Second action title should match");
4009- (void)testEditMenu_customActionShouldTriggerDelegateCallback {
4010 if (@available(iOS 16.0, *)) {
4013 OCMStub([mockEngine platformChannel]).andReturn(mockPlatformChannel);
4015 OCMStub([mockEngine flutterTextInputView:[OCMArg any]
4016 performContextMenuCustomActionWithActionID:
@"test-callback-id"
4017 textInputClient:123])
4018 .andDo((^(NSInvocation* invocation) {
4019 [mockPlatformChannel invokeMethod:@"ContextMenu.onPerformCustomAction"
4020 arguments:@[ @(123), @"test-callback-id" ]];
4027 [myViewController loadView];
4031 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4033 result:^(id _Nullable result){
4038 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4039 XCTestExpectation* expectation = [[XCTestExpectation alloc]
4040 initWithDescription:@"Custom action delegate callback should be called"];
4041 OCMStub(([mockPlatformChannel invokeMethod:
@"ContextMenu.onPerformCustomAction"
4042 arguments:@[ @(123),
@"test-callback-id" ]]))
4043 .andDo(^(NSInvocation* invocation) {
4044 [expectation fulfill];
4046 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4047 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4049 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4050 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4052 NSArray<NSDictionary*>* encodedItems = @[
4053 @{@"type" : @"custom", @"id" : @"test-callback-id", @"title" : @"Test Action"},
4056 BOOL shownEditMenu =
4057 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4058 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4060 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4061 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4062 suggestedActions:@[]];
4064 XCTAssertEqual(menu.children.count, 1UL,
@"Should have 1 custom menu item");
4065 UIAction* customAction = (UIAction*)menu.children[0];
4066 XCTAssertEqualObjects(customAction.title,
@"Test Action",
@"Action title should match");
4068 [myInputView.textInputDelegate flutterTextInputView:myInputView
4069 performContextMenuCustomActionWithActionID:@"test-callback-id"
4070 textInputClient:123];
4072 [
self waitForExpectations:@[ expectation ] timeout:1.0];
4073 OCMVerifyAll(mockPlatformChannel);
AssetResolverType
Identifies the type of AssetResolver an instance is.
A Mapping like NonOwnedMapping, but uses Free as its release proc.
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
G_BEGIN_DECLS GBytes * message
G_BEGIN_DECLS FlutterViewId view_id
FlutterDesktopBinaryReply callback
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
void flutterTextInputView:performAction:withClient:(FlutterTextInputView *textInputView,[performAction] FlutterTextInputAction action,[withClient] int client)
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
FlutterViewController * viewController
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
UIView< UITextInput > * textInputView()
UIIndirectScribbleInteractionDelegate UIViewController * viewController
BOOL showEditMenu:(ios(16.0) API_AVAILABLE)
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
UIAccessibilityNotifications receivedNotification
id receivedNotificationTarget
BOOL isAccessibilityFocused
instancetype positionWithIndex:(NSUInteger index)
UITextStorageDirection affinity
instancetype rangeWithNSRange:(NSRange range)
instancetype selectionRectWithRect:position:(CGRect rect,[position] NSUInteger position)
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)
NSArray< FlutterTextSelectionRect * > * selectionRects
UITextRange * markedTextRange
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
CGRect caretRectForPosition
const CGRect kInvalidFirstRect
NSDictionary * _passwordTemplate
FlutterViewController * viewController
FlutterTextInputPlugin * textInputPlugin
std::function< void()> closure