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;
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 {
52 self.receivedNotificationTarget = 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)
73 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
75 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
76 clearText:(BOOL)clearText
77 delayRemoval:(BOOL)delayRemoval;
78 - (NSArray<UIView*>*)textInputViews;
81 - (void)startLiveTextInput;
82 - (void)showKeyboardAndRemoveScreenshot;
90 NSDictionary* _template;
108 UIPasteboard.generalPasteboard.items = @[];
114 [textInputPlugin.autofillContext removeAllObjects];
115 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
116 [[[[textInputPlugin textInputView] superview] subviews]
117 makeObjectsPerformSelector:@selector(removeFromSuperview)];
122 - (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
125 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
126 [textInputPlugin handleMethodCall:setClientCall
127 result:^(id _Nullable result){
131 - (void)setTextInputShow {
134 [textInputPlugin handleMethodCall:setClientCall
135 result:^(id _Nullable result){
139 - (void)setTextInputHide {
142 [textInputPlugin handleMethodCall:setClientCall
143 result:^(id _Nullable result){
147 - (void)flushScheduledAsyncBlocks {
148 __block
bool done =
false;
149 XCTestExpectation* expectation =
150 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
151 dispatch_async(dispatch_get_main_queue(), ^{
154 dispatch_async(dispatch_get_main_queue(), ^{
156 [expectation fulfill];
158 [
self waitForExpectations:@[ expectation ] timeout:10];
161 - (NSMutableDictionary*)mutableTemplateCopy {
164 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
165 @"keyboardAppearance" :
@"Brightness.light",
166 @"obscureText" : @NO,
167 @"inputAction" :
@"TextInputAction.unspecified",
168 @"smartDashesType" :
@"0",
169 @"smartQuotesType" :
@"0",
170 @"autocorrect" : @YES,
171 @"enableInteractiveSelection" : @YES,
175 return [_template mutableCopy];
179 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
180 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
184 - (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
185 atIndex:(NSInteger)index {
188 withGranularity:UITextGranularityLine
189 inDirection:UITextLayoutDirectionRight];
194 - (void)updateConfig:(NSDictionary*)config {
197 [textInputPlugin handleMethodCall:updateConfigCall
198 result:^(id _Nullable result){
204 - (void)testWillNotCrashWhenViewControllerIsNil {
211 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
214 result:^(id _Nullable result) {
215 XCTAssertNil(result);
216 [expectation fulfill];
218 XCTAssertNil(inputPlugin.activeView);
219 [
self waitForExpectations:@[ expectation ] timeout:1.0];
222 - (void)testInvokeStartLiveTextInput {
227 result:^(id _Nullable result){
229 OCMVerify([mockPlugin startLiveTextInput]);
232 - (void)testNoDanglingEnginePointer {
242 weakFlutterEngine = flutterEngine;
243 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
245 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
246 weakFlutterTextInputPlugin = flutterTextInputPlugin;
250 NSDictionary* config =
self.mutableTemplateCopy;
253 arguments:@[ [NSNumber numberWithInt:123], config ]];
255 result:^(id _Nullable result){
257 currentView = flutterTextInputPlugin.activeView;
260 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
261 XCTAssertNotNil(currentView,
@"current view must not be nil");
263 XCTAssertNil(weakFlutterTextInputPlugin);
266 XCTAssertNil(currentView.textInputDelegate);
269 - (void)testSecureInput {
270 NSDictionary* config =
self.mutableTemplateCopy;
271 [config setValue:@"YES" forKey:@"obscureText"];
272 [
self setClientId:123 configuration:config];
275 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
282 XCTAssertTrue(inputView.secureTextEntry);
285 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
288 XCTAssertEqual(inputFields.count, 1ul);
296 XCTAssert(inputView.autofillId.length > 0);
299 - (void)testKeyboardType {
300 NSDictionary* config =
self.mutableTemplateCopy;
301 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
302 [
self setClientId:123 configuration:config];
305 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
310 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
313 - (void)testKeyboardTypeWebSearch {
314 NSDictionary* config =
self.mutableTemplateCopy;
315 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
316 [
self setClientId:123 configuration:config];
319 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
324 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
327 - (void)testKeyboardTypeTwitter {
328 NSDictionary* config =
self.mutableTemplateCopy;
329 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
330 [
self setClientId:123 configuration:config];
333 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
338 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
341 - (void)testVisiblePasswordUseAlphanumeric {
342 NSDictionary* config =
self.mutableTemplateCopy;
343 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
344 [
self setClientId:123 configuration:config];
347 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
352 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
355 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
356 NSDictionary* config =
self.mutableTemplateCopy;
357 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
358 [
self setClientId:123 configuration:config];
363 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
364 [
self setClientId:124 configuration:config];
369 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
373 if (@available(iOS 17.0, *)) {
375 OCMVerify(never(), [
engine flutterTextInputView:inputView
376 showAutocorrectionPromptRectForStart:0
380 OCMVerify([
engine flutterTextInputView:inputView
381 showAutocorrectionPromptRectForStart:0
387 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
389 __block
int updateCount = 0;
390 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
391 .andDo(^(NSInvocation* invocation) {
395 [inputView.text setString:@"Some initial text"];
396 XCTAssertEqual(updateCount, 0);
399 [inputView setSelectedTextRange:textRange];
400 XCTAssertEqual(updateCount, 1);
403 NSDictionary* config =
self.mutableTemplateCopy;
404 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
405 [config setValue:@(NO) forKey:@"obscureText"];
406 [config setValue:@(NO) forKey:@"enableDeltaModel"];
407 [inputView configureWithDictionary:config];
410 [inputView setSelectedTextRange:textRange];
412 XCTAssertEqual(updateCount, 1);
415 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
417 if (@available(iOS 17.0, *)) {
421 if (@available(iOS 14.0, *)) {
424 __block
int callCount = 0;
425 OCMStub([
engine flutterTextInputView:inputView
426 showAutocorrectionPromptRectForStart:0
429 .andDo(^(NSInvocation* invocation) {
435 XCTAssertEqual(callCount, 1);
437 UIScribbleInteraction* scribbleInteraction =
438 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
440 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
444 XCTAssertEqual(callCount, 1);
446 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
447 [inputView resetScribbleInteractionStatusIfEnding];
450 XCTAssertEqual(callCount, 2);
452 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
456 XCTAssertEqual(callCount, 2);
458 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
462 XCTAssertEqual(callCount, 2);
464 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
467 XCTAssertEqual(callCount, 3);
471 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
477 arguments:@[ @(123),
self.mutableTemplateCopy ]];
479 result:^(id _Nullable result){
486 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
490 arguments:@{@"transform" : yOffsetMatrix}];
492 result:^(id _Nullable result){
495 if (@available(iOS 17, *)) {
496 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
497 @"The input hider should overlap with the text on and after iOS 17");
500 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
501 @"The input hider should be on the origin of screen on and before iOS 16.");
505 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
511 toPosition:toPosition];
512 NSRange range = flutterRange.
range;
514 XCTAssertEqual(range.location, 0ul);
515 XCTAssertEqual(range.length, 2ul);
518 - (void)testTextInRange {
519 NSDictionary* config =
self.mutableTemplateCopy;
520 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
521 [
self setClientId:123 configuration:config];
522 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
525 [inputView insertText:@"test"];
528 NSString* substring = [inputView textInRange:range];
529 XCTAssertEqual(substring.length, 4ul);
532 substring = [inputView textInRange:range];
533 XCTAssertEqual(substring.length, 0ul);
536 - (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
537 NSDictionary* config =
self.mutableTemplateCopy;
538 [
self setClientId:123 configuration:config];
539 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
542 [inputView insertText:@"text"];
545 NSString* substring = [inputView textInRange:range];
546 XCTAssertNil(substring);
549 - (void)testStandardEditActions {
550 NSDictionary* config =
self.mutableTemplateCopy;
551 [
self setClientId:123 configuration:config];
552 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
555 [inputView insertText:@"aaaa"];
556 [inputView selectAll:nil];
558 [inputView insertText:@"bbbb"];
559 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
560 [inputView paste:nil];
561 [inputView selectAll:nil];
562 [inputView copy:nil];
563 [inputView paste:nil];
564 [inputView selectAll:nil];
565 [inputView delete:nil];
566 [inputView paste:nil];
567 [inputView paste:nil];
570 NSString* substring = [inputView textInRange:range];
571 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
574 - (void)testCanPerformActionForSelectActions {
575 NSDictionary* config =
self.mutableTemplateCopy;
576 [
self setClientId:123 configuration:config];
577 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
580 XCTAssertFalse([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
582 [inputView insertText:@"aaaa"];
584 XCTAssertTrue([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
587 - (void)testDeletingBackward {
588 NSDictionary* config =
self.mutableTemplateCopy;
589 [
self setClientId:123 configuration:config];
590 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
593 [inputView insertText:@"á ž¹ð Ÿ˜€ text 𠟥°ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦ð Ÿ‡ºð Ÿ‡³à ¸”à ¸µ "];
594 [inputView deleteBackward];
595 [inputView deleteBackward];
598 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³à¸”");
599 [inputView deleteBackward];
600 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³");
601 [inputView deleteBackward];
602 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦");
603 [inputView deleteBackward];
604 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
605 [inputView deleteBackward];
607 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
608 [inputView deleteBackward];
609 [inputView deleteBackward];
610 [inputView deleteBackward];
611 [inputView deleteBackward];
612 [inputView deleteBackward];
613 [inputView deleteBackward];
615 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
616 [inputView deleteBackward];
617 XCTAssertEqualObjects(inputView.text,
@"áž¹");
618 [inputView deleteBackward];
619 XCTAssertEqualObjects(inputView.text,
@"");
624 - (void)testSystemOnlyAddingPartialComposedCharacter {
625 NSDictionary* config =
self.mutableTemplateCopy;
626 [
self setClientId:123 configuration:config];
627 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
630 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
631 [inputView deleteBackward];
634 [inputView insertText:[@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)]];
635 [inputView insertText:@"ì •„"];
637 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ì•„");
640 [inputView deleteBackward];
643 [inputView insertText:@"𠟘€"];
644 [inputView deleteBackward];
646 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
647 [inputView insertText:@"ì •„"];
648 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
651 [inputView deleteBackward];
654 [inputView deleteBackward];
656 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
657 [inputView insertText:@"ì •„"];
659 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
662 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
663 NSDictionary* config =
self.mutableTemplateCopy;
664 [
self setClientId:123 configuration:config];
665 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
668 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
669 [inputView deleteBackward];
670 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
673 NSString* brokenEmoji = [@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)];
674 [inputView insertText:brokenEmoji];
675 [inputView insertText:@"ì •„"];
677 NSString* finalText = [NSString stringWithFormat:@"%@ì •„", brokenEmoji];
678 XCTAssertEqualObjects(inputView.text, finalText);
681 - (void)testPastingNonTextDisallowed {
682 NSDictionary* config =
self.mutableTemplateCopy;
683 [
self setClientId:123 configuration:config];
684 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
687 UIPasteboard.generalPasteboard.color = UIColor.redColor;
688 XCTAssertNil(UIPasteboard.generalPasteboard.string);
689 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
690 [inputView paste:nil];
692 XCTAssertEqualObjects(inputView.text,
@"");
695 - (void)testNoZombies {
702 [passwordView.textField description];
704 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
707 - (void)testInputViewCrash {
712 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
713 activeView = inputPlugin.activeView;
715 [activeView updateEditingState];
718 - (void)testDoNotReuseInputViews {
719 NSDictionary* config =
self.mutableTemplateCopy;
720 [
self setClientId:123 configuration:config];
722 [
self setClientId:456 configuration:config];
724 XCTAssertNotNil(currentView);
729 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
731 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
735 - (void)testPropagatePressEventsToViewController {
737 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
738 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
742 NSDictionary* config =
self.mutableTemplateCopy;
743 [
self setClientId:123 configuration:config];
745 [
self setTextInputShow];
747 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
748 withEvent:OCMClassMock([UIPressesEvent class])];
750 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
751 withEvent:[OCMArg isNotNil]]);
752 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
753 withEvent:[OCMArg isNotNil]]);
755 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
756 withEvent:OCMClassMock([UIPressesEvent class])];
758 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
759 withEvent:[OCMArg isNotNil]]);
760 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
761 withEvent:[OCMArg isNotNil]]);
764 - (void)testPropagatePressEventsToViewController2 {
766 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
767 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
771 NSDictionary* config =
self.mutableTemplateCopy;
772 [
self setClientId:123 configuration:config];
773 [
self setTextInputShow];
776 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
777 withEvent:OCMClassMock([UIPressesEvent class])];
779 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
780 withEvent:[OCMArg isNotNil]]);
781 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
782 withEvent:[OCMArg isNotNil]]);
785 [
self setClientId:321 configuration:config];
786 [
self setTextInputShow];
788 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
790 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
791 withEvent:OCMClassMock([UIPressesEvent class])];
793 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
794 withEvent:[OCMArg isNotNil]]);
795 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
796 withEvent:[OCMArg isNotNil]]);
799 - (void)testUpdateSecureTextEntry {
800 NSDictionary* config =
self.mutableTemplateCopy;
801 [config setValue:@"YES" forKey:@"obscureText"];
802 [
self setClientId:123 configuration:config];
804 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
807 __block
int callCount = 0;
808 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
812 XCTAssertTrue(inputView.isSecureTextEntry);
814 config =
self.mutableTemplateCopy;
815 [config setValue:@"NO" forKey:@"obscureText"];
816 [
self updateConfig:config];
818 XCTAssertEqual(callCount, 1);
819 XCTAssertFalse(inputView.isSecureTextEntry);
822 - (void)testInputActionContinueAction {
838 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
840 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
843 - (void)testDisablingAutocorrectDisablesSpellChecking {
847 NSDictionary* config =
self.mutableTemplateCopy;
848 [inputView configureWithDictionary:config];
850 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
851 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
853 [config setValue:@(NO) forKey:@"autocorrect"];
854 [inputView configureWithDictionary:config];
856 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
857 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
860 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
862 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
876 XCTAssertEqual(inputView.markedTextRange, nil);
879 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
883 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
884 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
885 if (@available(iOS 17, *)) {
886 XCTAssertFalse(respondsToInsertionPointColor);
888 XCTAssertTrue(respondsToInsertionPointColor);
892 #pragma mark - TextEditingDelta tests
893 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
895 inputView.enableDeltaModel = YES;
897 __block
int updateCount = 0;
899 [inputView insertText:@"text to insert"];
902 flutterTextInputView:inputView
903 updateEditingClient:0
904 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
905 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
906 isEqualToString:
@""]) &&
907 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
908 isEqualToString:
@"text to insert"]) &&
909 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
910 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
912 .andDo(^(NSInvocation* invocation) {
915 XCTAssertEqual(updateCount, 0);
917 [
self flushScheduledAsyncBlocks];
920 XCTAssertEqual(updateCount, 1);
922 [inputView deleteBackward];
923 OCMExpect([
engine flutterTextInputView:inputView
924 updateEditingClient:0
925 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
926 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
927 isEqualToString:
@"text to insert"]) &&
928 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
929 isEqualToString:
@""]) &&
930 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
932 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
935 .andDo(^(NSInvocation* invocation) {
938 [
self flushScheduledAsyncBlocks];
939 XCTAssertEqual(updateCount, 2);
942 OCMExpect([
engine flutterTextInputView:inputView
943 updateEditingClient:0
944 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
945 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
946 isEqualToString:
@"text to inser"]) &&
947 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
948 isEqualToString:
@""]) &&
949 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
951 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
954 .andDo(^(NSInvocation* invocation) {
957 [
self flushScheduledAsyncBlocks];
958 XCTAssertEqual(updateCount, 3);
961 withText:@"replace text"];
964 flutterTextInputView:inputView
965 updateEditingClient:0
966 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
967 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
968 isEqualToString:
@"text to inser"]) &&
969 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
970 isEqualToString:
@"replace text"]) &&
971 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
972 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
974 .andDo(^(NSInvocation* invocation) {
977 [
self flushScheduledAsyncBlocks];
978 XCTAssertEqual(updateCount, 4);
980 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
981 OCMExpect([
engine flutterTextInputView:inputView
982 updateEditingClient:0
983 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
984 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
985 isEqualToString:
@"replace textext to inser"]) &&
986 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
987 isEqualToString:
@"marked text"]) &&
988 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
990 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
993 .andDo(^(NSInvocation* invocation) {
996 [
self flushScheduledAsyncBlocks];
997 XCTAssertEqual(updateCount, 5);
999 [inputView unmarkText];
1001 flutterTextInputView:inputView
1002 updateEditingClient:0
1003 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1004 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1005 isEqualToString:
@"replace textmarked textext to inser"]) &&
1006 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1007 isEqualToString:
@""]) &&
1008 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1010 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1013 .andDo(^(NSInvocation* invocation) {
1016 [
self flushScheduledAsyncBlocks];
1018 XCTAssertEqual(updateCount, 6);
1022 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1025 inputView.enableDeltaModel = YES;
1028 OCMExpect([
engine flutterTextInputView:inputView
1029 updateEditingClient:0
1030 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1031 NSArray* deltas = state[@"deltas"];
1032 NSDictionary* firstDelta = deltas[0];
1033 NSDictionary* secondDelta = deltas[1];
1034 NSDictionary* thirdDelta = deltas[2];
1035 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1036 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1037 [firstDelta[@"deltaStart"] intValue] == 0 &&
1038 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1039 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1040 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1041 [secondDelta[@"deltaStart"] intValue] == 0 &&
1042 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1043 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1044 [thirdDelta[@"deltaText"] isEqualToString:@"â €”"] &&
1045 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1046 [thirdDelta[@"deltaEnd"] intValue] == 0;
1050 [inputView insertText:@"-"];
1051 [inputView deleteBackward];
1052 [inputView insertText:@"â €”"];
1054 [
self flushScheduledAsyncBlocks];
1058 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1060 inputView.enableDeltaModel = YES;
1062 __block
int updateCount = 0;
1063 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1064 .andDo(^(NSInvocation* invocation) {
1068 [inputView.text setString:@"Some initial text"];
1069 XCTAssertEqual(updateCount, 0);
1072 inputView.markedTextRange = range;
1073 inputView.selectedTextRange = nil;
1074 [
self flushScheduledAsyncBlocks];
1075 XCTAssertEqual(updateCount, 1);
1077 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1079 flutterTextInputView:inputView
1080 updateEditingClient:0
1081 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1082 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1083 isEqualToString:
@"Some initial text"]) &&
1084 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1085 isEqualToString:
@"new marked text."]) &&
1086 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1087 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1089 [
self flushScheduledAsyncBlocks];
1090 XCTAssertEqual(updateCount, 2);
1093 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1095 inputView.enableDeltaModel = YES;
1097 __block
int updateCount = 0;
1098 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1099 .andDo(^(NSInvocation* invocation) {
1103 [inputView.text setString:@"Some initial text"];
1104 [
self flushScheduledAsyncBlocks];
1105 XCTAssertEqual(updateCount, 0);
1108 inputView.markedTextRange = range;
1109 inputView.selectedTextRange = nil;
1110 [
self flushScheduledAsyncBlocks];
1111 XCTAssertEqual(updateCount, 1);
1113 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1115 flutterTextInputView:inputView
1116 updateEditingClient:0
1117 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1118 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1119 isEqualToString:
@"Some initial text"]) &&
1120 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1121 isEqualToString:
@"text."]) &&
1122 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1123 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1125 [
self flushScheduledAsyncBlocks];
1126 XCTAssertEqual(updateCount, 2);
1129 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1131 inputView.enableDeltaModel = YES;
1133 __block
int updateCount = 0;
1134 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1135 .andDo(^(NSInvocation* invocation) {
1139 [inputView.text setString:@"Some initial text"];
1140 [
self flushScheduledAsyncBlocks];
1141 XCTAssertEqual(updateCount, 0);
1144 inputView.markedTextRange = range;
1145 inputView.selectedTextRange = nil;
1146 [
self flushScheduledAsyncBlocks];
1147 XCTAssertEqual(updateCount, 1);
1149 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1151 flutterTextInputView:inputView
1152 updateEditingClient:0
1153 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1154 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1155 isEqualToString:
@"Some initial text"]) &&
1156 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1157 isEqualToString:
@"tex"]) &&
1158 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1159 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1161 [
self flushScheduledAsyncBlocks];
1162 XCTAssertEqual(updateCount, 2);
1165 #pragma mark - EditingState tests
1167 - (void)testUITextInputCallsUpdateEditingStateOnce {
1170 __block
int updateCount = 0;
1171 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1172 .andDo(^(NSInvocation* invocation) {
1176 [inputView insertText:@"text to insert"];
1178 XCTAssertEqual(updateCount, 1);
1180 [inputView deleteBackward];
1181 XCTAssertEqual(updateCount, 2);
1184 XCTAssertEqual(updateCount, 3);
1187 withText:@"replace text"];
1188 XCTAssertEqual(updateCount, 4);
1190 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1191 XCTAssertEqual(updateCount, 5);
1193 [inputView unmarkText];
1194 XCTAssertEqual(updateCount, 6);
1197 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1199 inputView.enableDeltaModel = YES;
1201 __block
int updateCount = 0;
1202 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1203 .andDo(^(NSInvocation* invocation) {
1207 [inputView insertText:@"text to insert"];
1208 [
self flushScheduledAsyncBlocks];
1210 XCTAssertEqual(updateCount, 1);
1212 [inputView deleteBackward];
1213 [
self flushScheduledAsyncBlocks];
1214 XCTAssertEqual(updateCount, 2);
1217 [
self flushScheduledAsyncBlocks];
1218 XCTAssertEqual(updateCount, 3);
1221 withText:@"replace text"];
1222 [
self flushScheduledAsyncBlocks];
1223 XCTAssertEqual(updateCount, 4);
1225 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1226 [
self flushScheduledAsyncBlocks];
1227 XCTAssertEqual(updateCount, 5);
1229 [inputView unmarkText];
1230 [
self flushScheduledAsyncBlocks];
1231 XCTAssertEqual(updateCount, 6);
1234 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1237 __block
int updateCount = 0;
1238 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1239 .andDo(^(NSInvocation* invocation) {
1243 [inputView.text setString:@"BEFORE"];
1244 XCTAssertEqual(updateCount, 0);
1246 inputView.markedTextRange = nil;
1247 inputView.selectedTextRange = nil;
1248 XCTAssertEqual(updateCount, 1);
1251 XCTAssertEqual(updateCount, 1);
1252 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1253 XCTAssertEqual(updateCount, 1);
1254 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1255 XCTAssertEqual(updateCount, 1);
1259 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1260 XCTAssertEqual(updateCount, 1);
1262 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1263 XCTAssertEqual(updateCount, 1);
1267 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1268 XCTAssertEqual(updateCount, 1);
1270 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1271 XCTAssertEqual(updateCount, 1);
1274 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1276 inputView.enableDeltaModel = YES;
1278 __block
int updateCount = 0;
1279 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1280 .andDo(^(NSInvocation* invocation) {
1284 [inputView.text setString:@"BEFORE"];
1285 [
self flushScheduledAsyncBlocks];
1286 XCTAssertEqual(updateCount, 0);
1288 inputView.markedTextRange = nil;
1289 inputView.selectedTextRange = nil;
1290 [
self flushScheduledAsyncBlocks];
1291 XCTAssertEqual(updateCount, 1);
1294 XCTAssertEqual(updateCount, 1);
1295 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1296 [
self flushScheduledAsyncBlocks];
1297 XCTAssertEqual(updateCount, 1);
1299 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1300 [
self flushScheduledAsyncBlocks];
1301 XCTAssertEqual(updateCount, 1);
1305 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1306 [
self flushScheduledAsyncBlocks];
1307 XCTAssertEqual(updateCount, 1);
1310 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1311 [
self flushScheduledAsyncBlocks];
1312 XCTAssertEqual(updateCount, 1);
1316 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1317 [
self flushScheduledAsyncBlocks];
1318 XCTAssertEqual(updateCount, 1);
1321 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1322 [
self flushScheduledAsyncBlocks];
1323 XCTAssertEqual(updateCount, 1);
1326 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1329 __block
int updateCount = 0;
1330 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1331 .andDo(^(NSInvocation* invocation) {
1335 [inputView unmarkText];
1337 XCTAssertEqual(updateCount, 0);
1339 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1341 XCTAssertEqual(updateCount, 1);
1343 [inputView unmarkText];
1345 XCTAssertEqual(updateCount, 2);
1348 - (void)testCanCopyPasteWithScribbleEnabled {
1349 if (@available(iOS 14.0, *)) {
1350 NSDictionary* config =
self.mutableTemplateCopy;
1351 [
self setClientId:123 configuration:config];
1352 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1358 [mockInputView insertText:@"aaaa"];
1359 [mockInputView selectAll:nil];
1361 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1362 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1363 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1364 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1366 [mockInputView copy:NULL];
1367 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1368 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1369 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1370 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1374 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1375 if (@available(iOS 14.0, *)) {
1378 __block
int updateCount = 0;
1379 OCMStub([
engine flutterTextInputView:inputView
1380 updateEditingClient:0
1381 withState:[OCMArg isNotNil]])
1382 .andDo(^(NSInvocation* invocation) {
1386 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1388 XCTAssertEqual(updateCount, 1);
1390 UIScribbleInteraction* scribbleInteraction =
1391 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1393 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1394 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1396 XCTAssertEqual(updateCount, 1);
1398 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1399 [inputView resetScribbleInteractionStatusIfEnding];
1400 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1402 XCTAssertEqual(updateCount, 2);
1404 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1405 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1408 XCTAssertEqual(updateCount, 2);
1410 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1411 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1414 XCTAssertEqual(updateCount, 2);
1416 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1417 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1419 XCTAssertEqual(updateCount, 3);
1423 - (void)testUpdateEditingClientNegativeSelection {
1426 [inputView.text setString:@"SELECTION"];
1427 inputView.markedTextRange = nil;
1428 inputView.selectedTextRange = nil;
1430 [inputView setTextInputState:@{
1431 @"text" : @"SELECTION",
1432 @"selectionBase" : @-1,
1433 @"selectionExtent" : @-1
1435 [inputView updateEditingState];
1436 OCMVerify([
engine flutterTextInputView:inputView
1437 updateEditingClient:0
1438 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1439 return ([state[
@"selectionBase"] intValue]) == 0 &&
1440 ([state[
@"selectionExtent"] intValue] == 0);
1445 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1446 [inputView updateEditingState];
1447 OCMVerify([
engine flutterTextInputView:inputView
1448 updateEditingClient:0
1449 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1450 return ([state[
@"selectionBase"] intValue]) == 0 &&
1451 ([state[
@"selectionExtent"] intValue] == 0);
1455 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1456 [inputView updateEditingState];
1457 OCMVerify([
engine flutterTextInputView:inputView
1458 updateEditingClient:0
1459 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1460 return ([state[
@"selectionBase"] intValue]) == 0 &&
1461 ([state[
@"selectionExtent"] intValue] == 0);
1465 - (void)testUpdateEditingClientSelectionClamping {
1469 [inputView.text setString:@"SELECTION"];
1470 inputView.markedTextRange = nil;
1471 inputView.selectedTextRange = nil;
1474 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1475 [inputView updateEditingState];
1476 OCMVerify([
engine flutterTextInputView:inputView
1477 updateEditingClient:0
1478 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1479 return ([state[
@"selectionBase"] intValue]) == 0 &&
1480 ([state[
@"selectionExtent"] intValue] == 0);
1484 [inputView setTextInputState:@{
1485 @"text" : @"SELECTION",
1486 @"selectionBase" : @0,
1487 @"selectionExtent" : @9999
1489 [inputView updateEditingState];
1491 OCMVerify([
engine flutterTextInputView:inputView
1492 updateEditingClient:0
1493 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1494 return ([state[
@"selectionBase"] intValue]) == 0 &&
1495 ([state[
@"selectionExtent"] intValue] == 9);
1500 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1501 [inputView updateEditingState];
1502 OCMVerify([
engine flutterTextInputView:inputView
1503 updateEditingClient:0
1504 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1505 return ([state[
@"selectionBase"] intValue]) == 0 &&
1506 ([state[
@"selectionExtent"] intValue] == 1);
1510 [inputView setTextInputState:@{
1511 @"text" : @"SELECTION",
1512 @"selectionBase" : @9999,
1513 @"selectionExtent" : @9999
1515 [inputView updateEditingState];
1516 OCMVerify([
engine flutterTextInputView:inputView
1517 updateEditingClient:0
1518 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1519 return ([state[
@"selectionBase"] intValue]) == 9 &&
1520 ([state[
@"selectionExtent"] intValue] == 9);
1524 - (void)testInputViewsHasNonNilInputDelegate {
1525 if (@available(iOS 13.0, *)) {
1527 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1529 [inputView setTextInputClient:123];
1530 [inputView reloadInputViews];
1531 [inputView becomeFirstResponder];
1532 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1533 inputView.inputDelegate = nil;
1536 [mockInputView setTextInputState:@{
1537 @"text" : @"COMPOSING",
1538 @"composingBase" : @1,
1539 @"composingExtent" : @3
1541 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1542 [inputView removeFromSuperview];
1546 - (void)testInputViewsDoNotHaveUITextInteractions {
1547 if (@available(iOS 13.0, *)) {
1549 BOOL hasTextInteraction = NO;
1550 for (
id interaction in inputView.interactions) {
1551 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1552 if (hasTextInteraction) {
1556 XCTAssertFalse(hasTextInteraction);
1560 #pragma mark - UITextInput methods - Tests
1562 - (void)testUpdateFirstRectForRange {
1563 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1569 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1574 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1575 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1579 NSArray* affineMatrix = @[
1580 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1581 @(-6.0), @(3.0), @(9.0), @(1.0)
1585 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1587 [inputView setEditableTransform:yOffsetMatrix];
1589 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1592 CGRect testRect = CGRectMake(0, 0, 100, 100);
1593 [inputView setMarkedRect:testRect];
1595 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1596 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1598 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1601 [inputView setEditableTransform:zeroMatrix];
1603 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1604 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1607 [inputView setEditableTransform:yOffsetMatrix];
1608 [inputView setMarkedRect:testRect];
1609 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1612 [inputView setMarkedRect:kInvalidFirstRect];
1614 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1615 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1618 [inputView setEditableTransform:affineMatrix];
1619 [inputView setMarkedRect:testRect];
1621 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1623 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1624 const CGPoint offset = CGPointMake(113, 119);
1625 CGRect currentFrame = inputView.frame;
1626 currentFrame.origin = offset;
1627 inputView.frame = currentFrame;
1630 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1631 [inputView firstRectForRange:range]));
1634 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1636 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1641 [inputView setSelectionRects:@[
1650 if (@available(iOS 17, *)) {
1651 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1652 [inputView firstRectForRange:multiRectRange]));
1654 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1655 [inputView firstRectForRange:multiRectRange]));
1659 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1661 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1663 [inputView setSelectionRects:@[
1670 if (@available(iOS 17, *)) {
1671 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1672 [inputView firstRectForRange:singleRectRange]));
1674 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1679 if (@available(iOS 17, *)) {
1680 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1681 [inputView firstRectForRange:multiRectRange]));
1683 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1686 [inputView setTextInputState:@{@"text" : @"COM"}];
1688 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1691 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1693 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1695 [inputView setSelectionRects:@[
1702 if (@available(iOS 17, *)) {
1703 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1704 [inputView firstRectForRange:singleRectRange]));
1706 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1710 if (@available(iOS 17, *)) {
1711 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1712 [inputView firstRectForRange:multiRectRange]));
1714 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1717 [inputView setTextInputState:@{@"text" : @"COM"}];
1719 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1722 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1724 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1726 [inputView setSelectionRects:@[
1737 if (@available(iOS 17, *)) {
1738 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1739 [inputView firstRectForRange:singleRectRange]));
1741 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1746 if (@available(iOS 17, *)) {
1747 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1748 [inputView firstRectForRange:multiRectRange]));
1750 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1754 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1756 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1758 [inputView setSelectionRects:@[
1769 if (@available(iOS 17, *)) {
1770 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1771 [inputView firstRectForRange:singleRectRange]));
1773 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1777 if (@available(iOS 17, *)) {
1778 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1779 [inputView firstRectForRange:multiRectRange]));
1781 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1785 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1787 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1789 [inputView setSelectionRects:@[
1800 if (@available(iOS 17, *)) {
1801 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1802 [inputView firstRectForRange:multiRectRange]));
1804 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1808 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1810 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1812 [inputView setSelectionRects:@[
1823 if (@available(iOS 17, *)) {
1824 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1825 [inputView firstRectForRange:multiRectRange]));
1827 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1831 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1833 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1835 [inputView setSelectionRects:@[
1846 if (@available(iOS 17, *)) {
1847 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1848 [inputView firstRectForRange:multiRectRange]));
1850 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1854 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1856 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1858 [inputView setSelectionRects:@[
1869 if (@available(iOS 17, *)) {
1870 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1871 [inputView firstRectForRange:multiRectRange]));
1873 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1877 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1879 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1881 [inputView setSelectionRects:@[
1892 if (@available(iOS 17, *)) {
1893 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1894 [inputView firstRectForRange:multiRectRange]));
1896 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1900 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1902 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1904 [inputView setSelectionRects:@[
1915 if (@available(iOS 17, *)) {
1916 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1917 [inputView firstRectForRange:multiRectRange]));
1919 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1923 - (void)testClosestPositionToPoint {
1925 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1928 [inputView setSelectionRects:@[
1933 CGPoint point = CGPointMake(150, 150);
1934 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1935 XCTAssertEqual(UITextStorageDirectionBackward,
1940 [inputView setSelectionRects:@[
1947 point = CGPointMake(125, 150);
1948 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1949 XCTAssertEqual(UITextStorageDirectionForward,
1954 [inputView setSelectionRects:@[
1961 point = CGPointMake(125, 201);
1962 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1963 XCTAssertEqual(UITextStorageDirectionBackward,
1967 [inputView setSelectionRects:@[
1973 point = CGPointMake(125, 250);
1974 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1975 XCTAssertEqual(UITextStorageDirectionBackward,
1979 [inputView setSelectionRects:@[
1984 point = CGPointMake(110, 50);
1985 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1986 XCTAssertEqual(UITextStorageDirectionForward,
1991 [inputView beginFloatingCursorAtPoint:CGPointZero];
1992 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1993 XCTAssertEqual(UITextStorageDirectionForward,
1995 [inputView endFloatingCursor];
1998 - (void)testClosestPositionToPointRTL {
2000 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2002 [inputView setSelectionRects:@[
2018 XCTAssertEqual(0U, position.
index);
2019 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2021 XCTAssertEqual(1U, position.
index);
2022 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2024 XCTAssertEqual(1U, position.
index);
2025 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2027 XCTAssertEqual(2U, position.
index);
2028 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2030 XCTAssertEqual(2U, position.
index);
2031 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2033 XCTAssertEqual(3U, position.
index);
2034 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2036 XCTAssertEqual(3U, position.
index);
2037 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2040 - (void)testSelectionRectsForRange {
2042 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2044 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2045 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2046 [inputView setSelectionRects:@[
2055 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2056 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2057 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2061 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2062 XCTAssertTrue(CGRectEqualToRect(
2063 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2064 [inputView selectionRectsForRange:range][0].rect));
2067 - (void)testClosestPositionToPointWithinRange {
2069 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2072 [inputView setSelectionRects:@[
2079 CGPoint point = CGPointMake(125, 150);
2082 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2084 UITextStorageDirectionForward,
2085 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2088 [inputView setSelectionRects:@[
2095 point = CGPointMake(125, 150);
2098 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2100 UITextStorageDirectionForward,
2101 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2104 - (void)testClosestPositionToPointWithPartialSelectionRects {
2106 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2113 XCTAssertTrue(CGRectEqualToRect(
2116 affinity:UITextStorageDirectionForward]],
2117 CGRectMake(100, 0, 0, 100)));
2120 XCTAssertTrue(CGRectEqualToRect(
2123 affinity:UITextStorageDirectionForward]],
2127 #pragma mark - Floating Cursor - Tests
2129 - (void)testFloatingCursorDoesNotThrow {
2132 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2133 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2134 [inputView endFloatingCursor];
2135 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2136 [inputView endFloatingCursor];
2139 - (void)testFloatingCursor {
2141 [inputView setTextInputState:@{
2143 @"selectionBase" : @1,
2144 @"selectionExtent" : @1,
2155 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2158 XCTAssertTrue(CGRectEqualToRect(
2161 affinity:UITextStorageDirectionForward]],
2162 CGRectMake(0, 0, 0, 100)));
2165 XCTAssertTrue(CGRectEqualToRect(
2168 affinity:UITextStorageDirectionForward]],
2169 CGRectMake(100, 100, 0, 100)));
2170 XCTAssertTrue(CGRectEqualToRect(
2173 affinity:UITextStorageDirectionForward]],
2174 CGRectMake(200, 200, 0, 100)));
2175 XCTAssertTrue(CGRectEqualToRect(
2178 affinity:UITextStorageDirectionForward]],
2179 CGRectMake(300, 300, 0, 100)));
2182 XCTAssertTrue(CGRectEqualToRect(
2185 affinity:UITextStorageDirectionForward]],
2186 CGRectMake(400, 300, 0, 100)));
2188 XCTAssertTrue(CGRectEqualToRect(
2191 affinity:UITextStorageDirectionForward]],
2195 [inputView setTextInputState:@{
2197 @"selectionBase" : @2,
2198 @"selectionExtent" : @2,
2201 XCTAssertTrue(CGRectEqualToRect(
2204 affinity:UITextStorageDirectionBackward]],
2205 CGRectMake(0, 0, 0, 100)));
2208 XCTAssertTrue(CGRectEqualToRect(
2211 affinity:UITextStorageDirectionBackward]],
2212 CGRectMake(100, 0, 0, 100)));
2213 XCTAssertTrue(CGRectEqualToRect(
2216 affinity:UITextStorageDirectionBackward]],
2217 CGRectMake(200, 100, 0, 100)));
2218 XCTAssertTrue(CGRectEqualToRect(
2221 affinity:UITextStorageDirectionBackward]],
2222 CGRectMake(300, 200, 0, 100)));
2223 XCTAssertTrue(CGRectEqualToRect(
2226 affinity:UITextStorageDirectionBackward]],
2227 CGRectMake(400, 300, 0, 100)));
2229 XCTAssertTrue(CGRectEqualToRect(
2232 affinity:UITextStorageDirectionBackward]],
2237 CGRect initialBounds = inputView.bounds;
2238 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2239 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2240 OCMVerify([
engine flutterTextInputView:inputView
2241 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2243 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2244 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2245 ([state[
@"Y"] isEqualToNumber:@(0)]);
2248 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2249 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2250 OCMVerify([
engine flutterTextInputView:inputView
2251 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2253 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2254 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2255 ([state[
@"Y"] isEqualToNumber:@(333)]);
2258 [inputView endFloatingCursor];
2259 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2260 OCMVerify([
engine flutterTextInputView:inputView
2261 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2263 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2264 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2265 ([state[
@"Y"] isEqualToNumber:@(0)]);
2269 #pragma mark - UIKeyInput Overrides - Tests
2271 - (void)testInsertTextAddsPlaceholderSelectionRects {
2274 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2284 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2287 [inputView insertText:@"in"];
2315 #pragma mark - Autofill - Utilities
2317 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2320 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2321 @"keyboardAppearance" :
@"Brightness.light",
2322 @"obscureText" : @YES,
2323 @"inputAction" :
@"TextInputAction.unspecified",
2324 @"smartDashesType" :
@"0",
2325 @"smartQuotesType" :
@"0",
2326 @"autocorrect" : @YES
2330 return [_passwordTemplate mutableCopy];
2334 return [
self.installedInputViews
2335 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2338 - (void)commitAutofillContextAndVerify {
2342 [textInputPlugin handleMethodCall:methodCall
2343 result:^(id _Nullable result){
2346 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2351 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2355 #pragma mark - Autofill - Tests
2357 - (void)testDisablingAutofillOnInputClient {
2358 NSDictionary* config =
self.mutableTemplateCopy;
2359 [config setValue:@"YES" forKey:@"obscureText"];
2361 [
self setClientId:123 configuration:config];
2364 XCTAssertEqualObjects(inputView.textContentType,
@"");
2367 - (void)testAutofillEnabledByDefault {
2368 NSDictionary* config =
self.mutableTemplateCopy;
2369 [config setValue:@"NO" forKey:@"obscureText"];
2370 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2371 forKey:@"autofill"];
2373 [
self setClientId:123 configuration:config];
2376 XCTAssertNil(inputView.textContentType);
2379 - (void)testAutofillContext {
2380 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2383 @"uniqueIdentifier" : @"field1",
2384 @"hints" : @[ @"hint1" ],
2385 @"editingValue" : @{@"text" : @""}
2387 forKey:@"autofill"];
2389 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2391 @"uniqueIdentifier" : @"field2",
2392 @"hints" : @[ @"hint2" ],
2393 @"editingValue" : @{@"text" : @""}
2395 forKey:@"autofill"];
2397 NSMutableDictionary* config = [field1 mutableCopy];
2398 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2400 [
self setClientId:123 configuration:config];
2401 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2405 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2406 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2408 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2411 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2413 @"uniqueIdentifier" : @"field3",
2414 @"hints" : @[ @"hint3" ],
2415 @"editingValue" : @{@"text" : @""}
2417 forKey:@"autofill"];
2421 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2423 [
self setClientId:123 configuration:config];
2425 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2428 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2429 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2431 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2434 for (NSString* key in oldContext.allKeys) {
2435 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2439 config =
self.mutablePasswordTemplateCopy;
2442 [
self setClientId:124 configuration:config];
2443 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2445 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2448 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2449 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2452 for (NSString* key in oldContext.allKeys) {
2453 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2457 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2461 [
self setClientId:200 configuration:config];
2464 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2467 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2468 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2471 for (NSString* key in oldContext.allKeys) {
2472 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2475 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2478 - (void)testCommitAutofillContext {
2479 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2481 @"uniqueIdentifier" : @"field1",
2482 @"hints" : @[ @"hint1" ],
2483 @"editingValue" : @{@"text" : @""}
2485 forKey:@"autofill"];
2487 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2489 @"uniqueIdentifier" : @"field2",
2490 @"hints" : @[ @"hint2" ],
2491 @"editingValue" : @{@"text" : @""}
2493 forKey:@"autofill"];
2495 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2497 @"uniqueIdentifier" : @"field3",
2498 @"hints" : @[ @"hint3" ],
2499 @"editingValue" : @{@"text" : @""}
2501 forKey:@"autofill"];
2503 NSMutableDictionary* config = [field1 mutableCopy];
2504 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2506 [
self setClientId:123 configuration:config];
2507 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2509 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2511 [
self commitAutofillContextAndVerify];
2512 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2515 [
self setClientId:123 configuration:config];
2517 [
self setClientId:124 configuration:field3];
2518 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2520 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2521 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2524 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2526 [
self commitAutofillContextAndVerify];
2527 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2530 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2532 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2536 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2537 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2539 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2541 [
self commitAutofillContextAndVerify];
2542 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2545 - (void)testAutofillInputViews {
2546 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2548 @"uniqueIdentifier" : @"field1",
2549 @"hints" : @[ @"hint1" ],
2550 @"editingValue" : @{@"text" : @""}
2552 forKey:@"autofill"];
2554 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2556 @"uniqueIdentifier" : @"field2",
2557 @"hints" : @[ @"hint2" ],
2558 @"editingValue" : @{@"text" : @""}
2560 forKey:@"autofill"];
2562 NSMutableDictionary* config = [field1 mutableCopy];
2563 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2565 [
self setClientId:123 configuration:config];
2566 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2569 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2572 XCTAssertEqual(inputFields.count, 2ul);
2573 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2578 withText:@"Autofilled!"];
2579 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2582 OCMVerify([
engine flutterTextInputView:inactiveView
2583 updateEditingClient:0
2584 withState:[OCMArg isNotNil]
2585 withTag:
@"field2"]);
2588 - (void)testPasswordAutofillHack {
2589 NSDictionary* config =
self.mutableTemplateCopy;
2590 [config setValue:@"YES" forKey:@"obscureText"];
2591 [
self setClientId:123 configuration:config];
2594 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2598 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2601 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2604 - (void)testClearAutofillContextClearsSelection {
2605 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2606 NSDictionary* editingValue = @{
2607 @"text" :
@"REGULAR_TEXT_FIELD",
2608 @"composingBase" : @0,
2609 @"composingExtent" : @3,
2610 @"selectionBase" : @1,
2611 @"selectionExtent" : @4
2613 [regularField setValue:@{
2614 @"uniqueIdentifier" : @"field2",
2615 @"hints" : @[ @"hint2" ],
2616 @"editingValue" : editingValue,
2618 forKey:@"autofill"];
2619 [regularField addEntriesFromDictionary:editingValue];
2620 [
self setClientId:123 configuration:regularField];
2621 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2622 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2625 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2627 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2631 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2632 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2634 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2636 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2637 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2640 XCTAssert([oldInputView.text isEqualToString:
@""]);
2642 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2645 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2647 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2648 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2650 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2653 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2654 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2656 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2658 [
self commitAutofillContextAndVerify];
2661 - (void)testScribbleSetSelectionRects {
2662 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2663 NSDictionary* editingValue = @{
2664 @"text" :
@"REGULAR_TEXT_FIELD",
2665 @"composingBase" : @0,
2666 @"composingExtent" : @3,
2667 @"selectionBase" : @1,
2668 @"selectionExtent" : @4
2670 [regularField setValue:@{
2671 @"uniqueIdentifier" : @"field1",
2672 @"hints" : @[ @"hint2" ],
2673 @"editingValue" : editingValue,
2675 forKey:@"autofill"];
2676 [regularField addEntriesFromDictionary:editingValue];
2677 [
self setClientId:123 configuration:regularField];
2678 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2679 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2681 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2682 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2686 [textInputPlugin handleMethodCall:methodCall
2687 result:^(id _Nullable result){
2690 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2693 - (void)testDecommissionedViewAreNotReusedByAutofill {
2695 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2696 [configuration setValue:@{
2697 @"uniqueIdentifier" : @"field1",
2698 @"hints" : @[ UITextContentTypePassword ],
2699 @"editingValue" : @{@"text" : @""}
2701 forKey:@"autofill"];
2702 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2704 [
self setClientId:123 configuration:configuration];
2706 [
self setTextInputHide];
2709 [
self setClientId:124 configuration:configuration];
2713 XCTAssertNotNil(previousActiveView);
2717 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2724 #pragma mark - Accessibility - Tests
2726 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2727 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2730 [
self setTextInputShow];
2732 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2735 XCTAssertEqual([inputFields count], 1u);
2738 [
self setTextInputHide];
2740 inputFields =
self.installedInputViews;
2743 XCTAssertEqual([inputFields count], 0u);
2746 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2749 UIView* container = [[UIView alloc] init];
2750 UIAccessibilityElement* backing =
2751 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2752 inputView.backingTextInputAccessibilityObject = backing;
2754 inputView.isAccessibilityFocused = YES;
2755 [inputView accessibilityElementDidBecomeFocused];
2757 XCTAssertEqual(inputView.receivedNotification, UIAccessibilityScreenChangedNotification);
2758 XCTAssertEqual(inputView.receivedNotificationTarget, backing);
2761 - (void)testFlutterTokenizerCanParseLines {
2763 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2766 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2767 XCTAssertEqual(range.
range.location, 0u);
2768 XCTAssertEqual(range.
range.length, 0u);
2770 [inputView insertText:@"how are you\nI am fine, Thank you"];
2772 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2773 XCTAssertEqual(range.
range.location, 0u);
2774 XCTAssertEqual(range.
range.length, 11u);
2776 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2777 XCTAssertEqual(range.
range.location, 0u);
2778 XCTAssertEqual(range.
range.length, 11u);
2780 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2781 XCTAssertEqual(range.
range.location, 0u);
2782 XCTAssertEqual(range.
range.length, 11u);
2784 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2785 XCTAssertEqual(range.
range.location, 12u);
2786 XCTAssertEqual(range.
range.length, 20u);
2788 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2789 XCTAssertEqual(range.
range.location, 12u);
2790 XCTAssertEqual(range.
range.length, 20u);
2792 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2793 XCTAssertEqual(range.
range.location, 12u);
2794 XCTAssertEqual(range.
range.length, 20u);
2797 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2799 [inputView insertText:@"0123456789\n012345"];
2800 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2803 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2804 withGranularity:UITextGranularityLine
2805 inDirection:UITextStorageDirectionBackward];
2806 XCTAssertEqual(range.
range.location, 11u);
2807 XCTAssertEqual(range.
range.length, 6u);
2810 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2812 [inputView insertText:@"0123456789\n012345"];
2813 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2816 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2817 withGranularity:UITextGranularityLine
2818 inDirection:UITextStorageDirectionForward];
2819 if (@available(iOS 17.0, *)) {
2820 XCTAssertNil(range);
2822 XCTAssertEqual(range.
range.location, 11u);
2823 XCTAssertEqual(range.
range.length, 6u);
2827 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2829 [inputView insertText:@"0123456789\n012345"];
2830 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2835 withGranularity:UITextGranularityLine
2836 inDirection:UITextStorageDirectionForward];
2837 if (@available(iOS 17.0, *)) {
2838 XCTAssertNil(range);
2840 XCTAssertEqual(range.
range.location, 0u);
2841 XCTAssertEqual(range.
range.length, 0u);
2845 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2850 __weak UIView* activeView;
2855 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2858 result:^(id _Nullable result){
2864 result:^(id _Nullable result){
2866 XCTAssertNotNil(activeView);
2869 XCTAssertNotNil(activeView);
2872 - (void)testFlutterTextInputPluginHostViewNilCrash {
2875 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2878 - (void)testFlutterTextInputPluginHostViewNotNil {
2884 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2887 - (void)testSetPlatformViewClient {
2894 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2896 result:^(id _Nullable result){
2899 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2902 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2904 result:^(id _Nullable result){
2906 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2909 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
2910 if (@available(iOS 16.0, *)) {
2912 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2913 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
2914 @"editMenuInteraction setup delegate correctly");
2918 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
2919 if (@available(iOS 16.0, *)) {
2923 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
2927 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
2928 if (@available(iOS 16.0, *)) {
2933 [myViewController loadView];
2936 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2938 result:^(id _Nullable result){
2944 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2946 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2947 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2949 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2950 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2951 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2952 .andDo(^(NSInvocation* invocation) {
2954 [invocation retainArguments];
2955 UIEditMenuConfiguration* config;
2956 [invocation getArgument:&config atIndex:2];
2957 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
2958 @"UIEditMenuConfiguration must use automatic arrow direction.");
2959 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
2960 @"UIEditMenuConfiguration must have the correct point.");
2961 [expectation fulfill];
2964 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2965 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
2967 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2968 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2969 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2973 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
2974 if (@available(iOS 16.0, *)) {
2979 [myViewController loadView];
2983 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2985 result:^(id _Nullable result){
2991 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2993 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2994 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2996 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2997 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2998 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2999 .andDo(^(NSInvocation* invocation) {
3000 [expectation fulfill];
3003 myInputView.frame = CGRectMake(10, 20, 30, 40);
3004 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3005 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3007 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3008 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3009 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3012 [myInputView editMenuInteraction:mockInteraction
3013 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3015 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3016 @"targetRectForConfiguration must return the correct target rect.");
3020 - (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3021 if (@available(iOS 16.0, *)) {
3026 [myViewController loadView];
3030 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3032 result:^(id _Nullable result){
3038 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3040 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3041 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3043 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3044 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3045 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3046 .andDo(^(NSInvocation* invocation) {
3047 [expectation fulfill];
3050 myInputView.frame = CGRectMake(10, 20, 30, 40);
3051 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3052 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3054 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3055 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3056 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3058 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3060 action:@selector(copy:)
3062 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3064 action:@selector(paste:)
3066 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3068 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3069 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3070 suggestedActions:suggestedActions];
3071 XCTAssertEqualObjects(menu.children, suggestedActions,
3072 @"Must show suggested items by default.");
3076 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3077 if (@available(iOS 16.0, *)) {
3082 [myViewController loadView];
3086 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3088 result:^(id _Nullable result){
3094 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3096 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3097 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3099 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3100 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3101 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3102 .andDo(^(NSInvocation* invocation) {
3103 [expectation fulfill];
3106 myInputView.frame = CGRectMake(10, 20, 30, 40);
3107 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3108 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3110 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3111 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3113 BOOL shownEditMenu =
3114 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3115 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3116 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3118 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3120 action:@selector(copy:)
3122 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3124 action:@selector(paste:)
3126 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3128 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3129 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3130 suggestedActions:suggestedActions];
3132 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3133 XCTAssertEqualObjects(menu.children, expectedChildren);
3137 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3138 if (@available(iOS 16.0, *)) {
3143 [myViewController loadView];
3147 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3149 result:^(id _Nullable result){
3155 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3157 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3158 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3160 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3161 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3162 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3163 .andDo(^(NSInvocation* invocation) {
3164 [expectation fulfill];
3167 myInputView.frame = CGRectMake(10, 20, 30, 40);
3168 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3169 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3171 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3172 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3174 BOOL shownEditMenu =
3175 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3176 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3177 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3179 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3181 action:@selector(copy:)
3183 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3185 action:@selector(cut:)
3187 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3189 action:@selector(paste:)
3202 NSArray<UIMenuElement*>* suggestedActions = @[
3203 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3204 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3207 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3208 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3209 suggestedActions:suggestedActions];
3211 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3212 XCTAssertEqualObjects(menu.children, expectedActions);
3216 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3217 if (@available(iOS 16.0, *)) {
3222 [myViewController loadView];
3226 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3228 result:^(id _Nullable result){
3234 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3236 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3237 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3239 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3240 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3241 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3242 .andDo(^(NSInvocation* invocation) {
3243 [expectation fulfill];
3246 myInputView.frame = CGRectMake(10, 20, 30, 40);
3247 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3248 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3250 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3251 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3252 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3255 BOOL shownEditMenu =
3256 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3257 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3258 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3260 NSArray<UICommand*>* suggestedActions = @[
3261 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3264 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3265 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3266 suggestedActions:suggestedActions];
3267 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3269 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3270 @"Must create search web item in the tree.");
3271 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3272 @"Must create look up item in the tree.");
3273 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3274 @"Must create share item in the tree.");
3278 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3280 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3282 [inputView setTextInputClient:123];
3283 [inputView reloadInputViews];
3284 [inputView becomeFirstResponder];
3285 XCTAssert(inputView.isFirstResponder);
3287 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3288 [NSNotificationCenter.defaultCenter
3289 postNotificationName:UIKeyboardWillShowNotification
3291 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3295 [textInputPlugin handleMethodCall:onPointerMoveCall
3296 result:^(id _Nullable result){
3298 XCTAssertFalse(inputView.isFirstResponder);
3302 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3303 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3304 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3305 UIScene* scene = scenes.anyObject;
3306 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3307 UIWindowScene* windowScene = (UIWindowScene*)scene;
3308 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3309 UIWindow* window = windowScene.windows[0];
3310 [window addSubview:viewController.view];
3312 [viewController loadView];
3315 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3317 [inputView setTextInputClient:123];
3318 [inputView reloadInputViews];
3319 [inputView becomeFirstResponder];
3322 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3323 [subView removeFromSuperview];
3327 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3328 [NSNotificationCenter.defaultCenter
3329 postNotificationName:UIKeyboardWillShowNotification
3331 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3335 [textInputPlugin handleMethodCall:onPointerMoveCall
3336 result:^(id _Nullable result){
3339 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3340 [subView removeFromSuperview];
3345 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3346 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3347 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3348 UIScene* scene = scenes.anyObject;
3349 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3350 UIWindowScene* windowScene = (UIWindowScene*)scene;
3351 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3352 UIWindow* window = windowScene.windows[0];
3353 [window addSubview:viewController.view];
3355 [viewController loadView];
3358 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3360 [inputView setTextInputClient:123];
3361 [inputView reloadInputViews];
3362 [inputView becomeFirstResponder];
3364 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3365 [NSNotificationCenter.defaultCenter
3366 postNotificationName:UIKeyboardWillShowNotification
3368 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3372 [textInputPlugin handleMethodCall:onPointerMoveCall
3373 result:^(id _Nullable result){
3377 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3382 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3383 result:^(id _Nullable result){
3387 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3389 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3390 [subView removeFromSuperview];
3395 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3396 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3397 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3398 UIScene* scene = scenes.anyObject;
3399 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3400 UIWindowScene* windowScene = (UIWindowScene*)scene;
3401 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3402 UIWindow* window = windowScene.windows[0];
3403 [window addSubview:viewController.view];
3405 [viewController loadView];
3408 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3410 [inputView setTextInputClient:123];
3411 [inputView reloadInputViews];
3412 [inputView becomeFirstResponder];
3414 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3415 [NSNotificationCenter.defaultCenter
3416 postNotificationName:UIKeyboardWillShowNotification
3418 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3422 [textInputPlugin handleMethodCall:onPointerMoveCall
3423 result:^(id _Nullable result){
3426 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3431 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3432 result:^(id _Nullable result){
3435 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3440 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3441 result:^(id _Nullable result){
3444 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3445 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3446 [subView removeFromSuperview];
3451 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3453 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3454 [inputView setTextInputClient:123];
3455 [inputView reloadInputViews];
3456 [inputView becomeFirstResponder];
3458 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3459 XCTAssertEqualObjects(inputView, firstResponder);
3463 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3470 [subInputView addSubview:subFirstResponderInputView];
3471 [inputView addSubview:subInputView];
3472 [inputView addSubview:otherSubInputView];
3473 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3474 [inputView setTextInputClient:123];
3475 [inputView reloadInputViews];
3476 [subInputView setTextInputClient:123];
3477 [subInputView reloadInputViews];
3478 [otherSubInputView setTextInputClient:123];
3479 [otherSubInputView reloadInputViews];
3480 [subFirstResponderInputView setTextInputClient:123];
3481 [subFirstResponderInputView reloadInputViews];
3482 [subFirstResponderInputView becomeFirstResponder];
3484 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3485 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3489 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3491 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3492 [inputView setTextInputClient:123];
3493 [inputView reloadInputViews];
3495 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3496 XCTAssertNil(firstResponder);
3500 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3501 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3502 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3503 UIScene* scene = scenes.anyObject;
3504 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3505 UIWindowScene* windowScene = (UIWindowScene*)scene;
3506 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3507 UIWindow* window = windowScene.windows[0];
3508 [window addSubview:viewController.view];
3510 [viewController loadView];
3512 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3513 initWithDescription:
3514 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3515 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3516 .andDo(^(NSInvocation* invocation) {
3517 [expectation fulfill];
3519 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3520 [NSNotificationCenter.defaultCenter
3521 postNotificationName:UIKeyboardWillShowNotification
3523 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3527 [textInputPlugin handleMethodCall:initialMoveCall
3528 result:^(id _Nullable result){
3533 [textInputPlugin handleMethodCall:subsequentMoveCall
3534 result:^(id _Nullable result){
3540 [textInputPlugin handleMethodCall:pointerUpCall
3541 result:^(id _Nullable result){
3544 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3548 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3549 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3550 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3551 UIScene* scene = scenes.anyObject;
3552 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3553 UIWindowScene* windowScene = (UIWindowScene*)scene;
3554 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3555 UIWindow* window = windowScene.windows[0];
3556 [window addSubview:viewController.view];
3558 [viewController loadView];
3560 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3561 [NSNotificationCenter.defaultCenter
3562 postNotificationName:UIKeyboardWillShowNotification
3564 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3568 [textInputPlugin handleMethodCall:initialMoveCall
3569 result:^(id _Nullable result){
3574 [textInputPlugin handleMethodCall:subsequentMoveCall
3575 result:^(id _Nullable result){
3581 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3582 result:^(id _Nullable result){
3588 [textInputPlugin handleMethodCall:pointerUpCall
3589 result:^(id _Nullable result){
3591 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3592 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3594 XCTNSPredicateExpectation* expectation =
3595 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3596 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3600 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3601 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3602 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3603 UIScene* scene = scenes.anyObject;
3604 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3605 UIWindowScene* windowScene = (UIWindowScene*)scene;
3606 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3607 UIWindow* window = windowScene.windows[0];
3608 [window addSubview:viewController.view];
3610 [viewController loadView];
3613 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3615 [inputView setTextInputClient:123];
3616 [inputView reloadInputViews];
3617 [inputView becomeFirstResponder];
3619 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3620 [NSNotificationCenter.defaultCenter
3621 postNotificationName:UIKeyboardWillShowNotification
3623 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3627 [textInputPlugin handleMethodCall:initialMoveCall
3628 result:^(id _Nullable result){
3633 [textInputPlugin handleMethodCall:subsequentMoveCall
3634 result:^(id _Nullable result){
3640 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3641 result:^(id _Nullable result){
3647 [textInputPlugin handleMethodCall:pointerUpCall
3648 result:^(id _Nullable result){
3650 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3651 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3653 XCTNSPredicateExpectation* expectation =
3654 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3655 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3659 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3660 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3661 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3662 UIScene* scene = scenes.anyObject;
3663 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3664 UIWindowScene* windowScene = (UIWindowScene*)scene;
3665 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3666 UIWindow* window = windowScene.windows[0];
3667 [window addSubview:viewController.view];
3669 [viewController loadView];
3671 XCTestExpectation* expectation =
3672 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3673 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3674 [NSNotificationCenter.defaultCenter
3675 postNotificationName:UIKeyboardWillShowNotification
3677 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3681 [textInputPlugin handleMethodCall:initialMoveCall
3682 result:^(id _Nullable result){
3687 [textInputPlugin handleMethodCall:subsequentMoveCall
3688 result:^(id _Nullable result){
3693 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3694 result:^(id _Nullable result){
3701 handleMethodCall:pointerUpCall
3702 result:^(id _Nullable result) {
3703 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3704 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3705 keyboardFrame.origin.y);
3706 [expectation fulfill];
3711 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3712 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3713 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3714 UIScene* scene = scenes.anyObject;
3715 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3716 UIWindowScene* windowScene = (UIWindowScene*)scene;
3717 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3718 UIWindow* window = windowScene.windows[0];
3719 [window addSubview:viewController.view];
3721 [viewController loadView];
3723 XCTestExpectation* expectation =
3724 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3725 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3726 [NSNotificationCenter.defaultCenter
3727 postNotificationName:UIKeyboardWillShowNotification
3729 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3733 [textInputPlugin handleMethodCall:initialMoveCall
3734 result:^(id _Nullable result){
3739 [textInputPlugin handleMethodCall:subsequentMoveCall
3740 result:^(id _Nullable result){
3747 handleMethodCall:pointerUpCall
3748 result:^(id _Nullable result) {
3749 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3750 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3751 [expectation fulfill];
3755 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3756 [UIView setAnimationsEnabled:YES];
3757 [textInputPlugin showKeyboardAndRemoveScreenshot];
3759 UIView.areAnimationsEnabled,
3760 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3763 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3764 [UIView setAnimationsEnabled:YES];
3765 [textInputPlugin showKeyboardAndRemoveScreenshot];
3767 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3769 return UIView.areAnimationsEnabled;
3771 XCTNSPredicateExpectation* expectation =
3772 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3773 [
self waitForExpectations:@[ expectation ] timeout:10.0];
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
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
FlutterViewController * viewController
void flutterTextInputView:performAction:withClient:(FlutterTextInputView *textInputView,[performAction] FlutterTextInputAction action,[withClient] int client)
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
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)