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;
40 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target;
47 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
49 self.receivedNotificationTarget = target;
52 - (BOOL)accessibilityElementIsFocused {
53 return _isAccessibilityFocused;
59 @property(nonatomic, strong) UITextField*
textField;
64 @property(nonatomic, readonly) UIView* inputHider;
65 @property(nonatomic, readonly) UIView* keyboardViewContainer;
66 @property(nonatomic, readonly) UIView* keyboardView;
67 @property(nonatomic, assign) UIView* cachedFirstResponder;
68 @property(nonatomic, readonly) CGRect keyboardRect;
69 @property(nonatomic, readonly)
70 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
72 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
73 clearText:(BOOL)clearText
74 delayRemoval:(BOOL)delayRemoval;
75 - (NSArray<UIView*>*)textInputViews;
78 - (void)startLiveTextInput;
79 - (void)showKeyboardAndRemoveScreenshot;
87 NSDictionary* _template;
105 UIPasteboard.generalPasteboard.items = @[];
111 [textInputPlugin.autofillContext removeAllObjects];
112 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
113 [[[[textInputPlugin textInputView] superview] subviews]
114 makeObjectsPerformSelector:@selector(removeFromSuperview)];
119 - (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
122 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
123 [textInputPlugin handleMethodCall:setClientCall
124 result:^(id _Nullable result){
128 - (void)setTextInputShow {
131 [textInputPlugin handleMethodCall:setClientCall
132 result:^(id _Nullable result){
136 - (void)setTextInputHide {
139 [textInputPlugin handleMethodCall:setClientCall
140 result:^(id _Nullable result){
144 - (void)flushScheduledAsyncBlocks {
145 __block
bool done =
false;
146 XCTestExpectation* expectation =
147 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
148 dispatch_async(dispatch_get_main_queue(), ^{
151 dispatch_async(dispatch_get_main_queue(), ^{
153 [expectation fulfill];
155 [
self waitForExpectations:@[ expectation ] timeout:10];
158 - (NSMutableDictionary*)mutableTemplateCopy {
161 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
162 @"keyboardAppearance" :
@"Brightness.light",
163 @"obscureText" : @NO,
164 @"inputAction" :
@"TextInputAction.unspecified",
165 @"smartDashesType" :
@"0",
166 @"smartQuotesType" :
@"0",
167 @"autocorrect" : @YES,
168 @"enableInteractiveSelection" : @YES,
172 return [_template mutableCopy];
176 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
177 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
181 - (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
182 atIndex:(NSInteger)index {
185 withGranularity:UITextGranularityLine
186 inDirection:UITextLayoutDirectionRight];
191 - (void)updateConfig:(NSDictionary*)config {
194 [textInputPlugin handleMethodCall:updateConfigCall
195 result:^(id _Nullable result){
201 - (void)testWillNotCrashWhenViewControllerIsNil {
208 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
211 result:^(id _Nullable result) {
212 XCTAssertNil(result);
213 [expectation fulfill];
215 XCTAssertNil(inputPlugin.activeView);
216 [
self waitForExpectations:@[ expectation ] timeout:1.0];
219 - (void)testInvokeStartLiveTextInput {
224 result:^(id _Nullable result){
226 OCMVerify([mockPlugin startLiveTextInput]);
229 - (void)testNoDanglingEnginePointer {
239 weakFlutterEngine = flutterEngine;
240 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
242 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
243 weakFlutterTextInputPlugin = flutterTextInputPlugin;
247 NSDictionary* config =
self.mutableTemplateCopy;
250 arguments:@[ [NSNumber numberWithInt:123], config ]];
252 result:^(id _Nullable result){
254 currentView = flutterTextInputPlugin.activeView;
257 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
258 XCTAssertNotNil(currentView,
@"current view must not be nil");
260 XCTAssertNil(weakFlutterTextInputPlugin);
263 XCTAssertNil(currentView.textInputDelegate);
266 - (void)testSecureInput {
267 NSDictionary* config =
self.mutableTemplateCopy;
268 [config setValue:@"YES" forKey:@"obscureText"];
269 [
self setClientId:123 configuration:config];
272 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
279 XCTAssertTrue(inputView.secureTextEntry);
282 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
285 XCTAssertEqual(inputFields.count, 1ul);
293 XCTAssert(inputView.autofillId.length > 0);
296 - (void)testKeyboardType {
297 NSDictionary* config =
self.mutableTemplateCopy;
298 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
299 [
self setClientId:123 configuration:config];
302 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
307 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
310 - (void)testKeyboardTypeWebSearch {
311 NSDictionary* config =
self.mutableTemplateCopy;
312 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
313 [
self setClientId:123 configuration:config];
316 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
321 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
324 - (void)testKeyboardTypeTwitter {
325 NSDictionary* config =
self.mutableTemplateCopy;
326 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
327 [
self setClientId:123 configuration:config];
330 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
335 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
338 - (void)testVisiblePasswordUseAlphanumeric {
339 NSDictionary* config =
self.mutableTemplateCopy;
340 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
341 [
self setClientId:123 configuration:config];
344 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
349 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
352 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
353 NSDictionary* config =
self.mutableTemplateCopy;
354 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
355 [
self setClientId:123 configuration:config];
360 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
361 [
self setClientId:124 configuration:config];
366 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
370 if (@available(iOS 17.0, *)) {
372 OCMVerify(never(), [
engine flutterTextInputView:inputView
373 showAutocorrectionPromptRectForStart:0
377 OCMVerify([
engine flutterTextInputView:inputView
378 showAutocorrectionPromptRectForStart:0
384 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
386 __block
int updateCount = 0;
387 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
388 .andDo(^(NSInvocation* invocation) {
392 [inputView.text setString:@"Some initial text"];
393 XCTAssertEqual(updateCount, 0);
396 [inputView setSelectedTextRange:textRange];
397 XCTAssertEqual(updateCount, 1);
400 NSDictionary* config =
self.mutableTemplateCopy;
401 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
402 [config setValue:@(NO) forKey:@"obscureText"];
403 [config setValue:@(NO) forKey:@"enableDeltaModel"];
404 [inputView configureWithDictionary:config];
407 [inputView setSelectedTextRange:textRange];
409 XCTAssertEqual(updateCount, 1);
412 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
414 if (@available(iOS 17.0, *)) {
418 if (@available(iOS 14.0, *)) {
421 __block
int callCount = 0;
422 OCMStub([
engine flutterTextInputView:inputView
423 showAutocorrectionPromptRectForStart:0
426 .andDo(^(NSInvocation* invocation) {
432 XCTAssertEqual(callCount, 1);
434 UIScribbleInteraction* scribbleInteraction =
435 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
437 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
441 XCTAssertEqual(callCount, 1);
443 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
444 [inputView resetScribbleInteractionStatusIfEnding];
447 XCTAssertEqual(callCount, 2);
449 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
453 XCTAssertEqual(callCount, 2);
455 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
459 XCTAssertEqual(callCount, 2);
461 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
464 XCTAssertEqual(callCount, 3);
468 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
474 arguments:@[ @(123),
self.mutableTemplateCopy ]];
476 result:^(id _Nullable result){
483 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
487 arguments:@{@"transform" : yOffsetMatrix}];
489 result:^(id _Nullable result){
492 if (@available(iOS 17, *)) {
493 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
494 @"The input hider should overlap with the text on and after iOS 17");
497 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
498 @"The input hider should be on the origin of screen on and before iOS 16.");
502 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
508 toPosition:toPosition];
509 NSRange range = flutterRange.
range;
511 XCTAssertEqual(range.location, 0ul);
512 XCTAssertEqual(range.length, 2ul);
515 - (void)testTextInRange {
516 NSDictionary* config =
self.mutableTemplateCopy;
517 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
518 [
self setClientId:123 configuration:config];
519 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
522 [inputView insertText:@"test"];
525 NSString* substring = [inputView textInRange:range];
526 XCTAssertEqual(substring.length, 4ul);
529 substring = [inputView textInRange:range];
530 XCTAssertEqual(substring.length, 0ul);
533 - (void)testStandardEditActions {
534 NSDictionary* config =
self.mutableTemplateCopy;
535 [
self setClientId:123 configuration:config];
536 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
539 [inputView insertText:@"aaaa"];
540 [inputView selectAll:nil];
542 [inputView insertText:@"bbbb"];
543 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
544 [inputView paste:nil];
545 [inputView selectAll:nil];
546 [inputView copy:nil];
547 [inputView paste:nil];
548 [inputView selectAll:nil];
549 [inputView delete:nil];
550 [inputView paste:nil];
551 [inputView paste:nil];
554 NSString* substring = [inputView textInRange:range];
555 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
558 - (void)testCanPerformActionForSelectActions {
559 NSDictionary* config =
self.mutableTemplateCopy;
560 [
self setClientId:123 configuration:config];
561 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
564 XCTAssertFalse([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
566 [inputView insertText:@"aaaa"];
568 XCTAssertTrue([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
571 - (void)testDeletingBackward {
572 NSDictionary* config =
self.mutableTemplateCopy;
573 [
self setClientId:123 configuration:config];
574 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
577 [inputView insertText:@"� ��� ��� text � ���� ���� ��� ���� ��� ���� ��� ���� ���� ���� ��� �� "];
578 [inputView deleteBackward];
579 [inputView deleteBackward];
582 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
583 [inputView deleteBackward];
584 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
585 [inputView deleteBackward];
586 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
587 [inputView deleteBackward];
588 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
589 [inputView deleteBackward];
591 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
592 [inputView deleteBackward];
593 [inputView deleteBackward];
594 [inputView deleteBackward];
595 [inputView deleteBackward];
596 [inputView deleteBackward];
597 [inputView deleteBackward];
599 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
600 [inputView deleteBackward];
601 XCTAssertEqualObjects(inputView.text,
@"ឹ");
602 [inputView deleteBackward];
603 XCTAssertEqualObjects(inputView.text,
@"");
608 - (void)testSystemOnlyAddingPartialComposedCharacter {
609 NSDictionary* config =
self.mutableTemplateCopy;
610 [
self setClientId:123 configuration:config];
611 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
614 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
615 [inputView deleteBackward];
618 [inputView insertText:[@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)]];
619 [inputView insertText:@"� ��"];
621 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
624 [inputView deleteBackward];
627 [inputView insertText:@"� ���"];
628 [inputView deleteBackward];
630 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
631 [inputView insertText:@"� ��"];
632 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
635 [inputView deleteBackward];
638 [inputView deleteBackward];
640 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
641 [inputView insertText:@"� ��"];
643 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
646 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
647 NSDictionary* config =
self.mutableTemplateCopy;
648 [
self setClientId:123 configuration:config];
649 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
652 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
653 [inputView deleteBackward];
654 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
657 NSString* brokenEmoji = [@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)];
658 [inputView insertText:brokenEmoji];
659 [inputView insertText:@"� ��"];
661 NSString* finalText = [NSString stringWithFormat:@"%@� ��", brokenEmoji];
662 XCTAssertEqualObjects(inputView.text, finalText);
665 - (void)testPastingNonTextDisallowed {
666 NSDictionary* config =
self.mutableTemplateCopy;
667 [
self setClientId:123 configuration:config];
668 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
671 UIPasteboard.generalPasteboard.color = UIColor.redColor;
672 XCTAssertNil(UIPasteboard.generalPasteboard.string);
673 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
674 [inputView paste:nil];
676 XCTAssertEqualObjects(inputView.text,
@"");
679 - (void)testNoZombies {
686 [passwordView.textField description];
688 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
691 - (void)testInputViewCrash {
696 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
697 activeView = inputPlugin.activeView;
699 [activeView updateEditingState];
702 - (void)testDoNotReuseInputViews {
703 NSDictionary* config =
self.mutableTemplateCopy;
704 [
self setClientId:123 configuration:config];
706 [
self setClientId:456 configuration:config];
708 XCTAssertNotNil(currentView);
713 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
715 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
719 - (void)testPropagatePressEventsToViewController {
721 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
722 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
726 NSDictionary* config =
self.mutableTemplateCopy;
727 [
self setClientId:123 configuration:config];
729 [
self setTextInputShow];
731 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
732 withEvent:OCMClassMock([UIPressesEvent class])];
734 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
735 withEvent:[OCMArg isNotNil]]);
736 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
737 withEvent:[OCMArg isNotNil]]);
739 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
740 withEvent:OCMClassMock([UIPressesEvent class])];
742 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
743 withEvent:[OCMArg isNotNil]]);
744 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
745 withEvent:[OCMArg isNotNil]]);
748 - (void)testPropagatePressEventsToViewController2 {
750 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
751 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
755 NSDictionary* config =
self.mutableTemplateCopy;
756 [
self setClientId:123 configuration:config];
757 [
self setTextInputShow];
760 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
761 withEvent:OCMClassMock([UIPressesEvent class])];
763 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
764 withEvent:[OCMArg isNotNil]]);
765 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
766 withEvent:[OCMArg isNotNil]]);
769 [
self setClientId:321 configuration:config];
770 [
self setTextInputShow];
772 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
774 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
775 withEvent:OCMClassMock([UIPressesEvent class])];
777 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
778 withEvent:[OCMArg isNotNil]]);
779 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
780 withEvent:[OCMArg isNotNil]]);
783 - (void)testUpdateSecureTextEntry {
784 NSDictionary* config =
self.mutableTemplateCopy;
785 [config setValue:@"YES" forKey:@"obscureText"];
786 [
self setClientId:123 configuration:config];
788 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
791 __block
int callCount = 0;
792 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
796 XCTAssertTrue(inputView.isSecureTextEntry);
798 config =
self.mutableTemplateCopy;
799 [config setValue:@"NO" forKey:@"obscureText"];
800 [
self updateConfig:config];
802 XCTAssertEqual(callCount, 1);
803 XCTAssertFalse(inputView.isSecureTextEntry);
806 - (void)testInputActionContinueAction {
822 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
824 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
827 - (void)testDisablingAutocorrectDisablesSpellChecking {
831 NSDictionary* config =
self.mutableTemplateCopy;
832 [inputView configureWithDictionary:config];
834 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
835 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
837 [config setValue:@(NO) forKey:@"autocorrect"];
838 [inputView configureWithDictionary:config];
840 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
841 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
844 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
846 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
860 XCTAssertEqual(inputView.markedTextRange, nil);
863 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
867 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
868 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
869 if (@available(iOS 17, *)) {
870 XCTAssertFalse(respondsToInsertionPointColor);
872 XCTAssertTrue(respondsToInsertionPointColor);
876 #pragma mark - TextEditingDelta tests
877 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
879 inputView.enableDeltaModel = YES;
881 __block
int updateCount = 0;
883 [inputView insertText:@"text to insert"];
886 flutterTextInputView:inputView
887 updateEditingClient:0
888 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
889 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
890 isEqualToString:
@""]) &&
891 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
892 isEqualToString:
@"text to insert"]) &&
893 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
894 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
896 .andDo(^(NSInvocation* invocation) {
899 XCTAssertEqual(updateCount, 0);
901 [
self flushScheduledAsyncBlocks];
904 XCTAssertEqual(updateCount, 1);
906 [inputView deleteBackward];
907 OCMExpect([
engine flutterTextInputView:inputView
908 updateEditingClient:0
909 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
910 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
911 isEqualToString:
@"text to insert"]) &&
912 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
913 isEqualToString:
@""]) &&
914 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
916 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
919 .andDo(^(NSInvocation* invocation) {
922 [
self flushScheduledAsyncBlocks];
923 XCTAssertEqual(updateCount, 2);
926 OCMExpect([
engine flutterTextInputView:inputView
927 updateEditingClient:0
928 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
929 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
930 isEqualToString:
@"text to inser"]) &&
931 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
932 isEqualToString:
@""]) &&
933 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
935 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
938 .andDo(^(NSInvocation* invocation) {
941 [
self flushScheduledAsyncBlocks];
942 XCTAssertEqual(updateCount, 3);
945 withText:@"replace text"];
948 flutterTextInputView:inputView
949 updateEditingClient:0
950 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
951 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
952 isEqualToString:
@"text to inser"]) &&
953 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
954 isEqualToString:
@"replace text"]) &&
955 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
956 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
958 .andDo(^(NSInvocation* invocation) {
961 [
self flushScheduledAsyncBlocks];
962 XCTAssertEqual(updateCount, 4);
964 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
965 OCMExpect([
engine flutterTextInputView:inputView
966 updateEditingClient:0
967 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
968 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
969 isEqualToString:
@"replace textext to inser"]) &&
970 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
971 isEqualToString:
@"marked text"]) &&
972 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
974 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
977 .andDo(^(NSInvocation* invocation) {
980 [
self flushScheduledAsyncBlocks];
981 XCTAssertEqual(updateCount, 5);
983 [inputView unmarkText];
985 flutterTextInputView:inputView
986 updateEditingClient:0
987 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
988 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
989 isEqualToString:
@"replace textmarked textext to inser"]) &&
990 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
991 isEqualToString:
@""]) &&
992 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
994 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
997 .andDo(^(NSInvocation* invocation) {
1000 [
self flushScheduledAsyncBlocks];
1002 XCTAssertEqual(updateCount, 6);
1006 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1009 inputView.enableDeltaModel = YES;
1012 OCMExpect([
engine flutterTextInputView:inputView
1013 updateEditingClient:0
1014 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1015 NSArray* deltas = state[@"deltas"];
1016 NSDictionary* firstDelta = deltas[0];
1017 NSDictionary* secondDelta = deltas[1];
1018 NSDictionary* thirdDelta = deltas[2];
1019 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1020 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1021 [firstDelta[@"deltaStart"] intValue] == 0 &&
1022 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1023 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1024 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1025 [secondDelta[@"deltaStart"] intValue] == 0 &&
1026 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1027 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1028 [thirdDelta[@"deltaText"] isEqualToString:@"� ��"] &&
1029 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1030 [thirdDelta[@"deltaEnd"] intValue] == 0;
1034 [inputView insertText:@"-"];
1035 [inputView deleteBackward];
1036 [inputView insertText:@"� ��"];
1038 [
self flushScheduledAsyncBlocks];
1042 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1044 inputView.enableDeltaModel = YES;
1046 __block
int updateCount = 0;
1047 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1048 .andDo(^(NSInvocation* invocation) {
1052 [inputView.text setString:@"Some initial text"];
1053 XCTAssertEqual(updateCount, 0);
1056 inputView.markedTextRange = range;
1057 inputView.selectedTextRange = nil;
1058 [
self flushScheduledAsyncBlocks];
1059 XCTAssertEqual(updateCount, 1);
1061 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1063 flutterTextInputView:inputView
1064 updateEditingClient:0
1065 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1066 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1067 isEqualToString:
@"Some initial text"]) &&
1068 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1069 isEqualToString:
@"new marked text."]) &&
1070 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1071 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1073 [
self flushScheduledAsyncBlocks];
1074 XCTAssertEqual(updateCount, 2);
1077 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1079 inputView.enableDeltaModel = YES;
1081 __block
int updateCount = 0;
1082 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1083 .andDo(^(NSInvocation* invocation) {
1087 [inputView.text setString:@"Some initial text"];
1088 [
self flushScheduledAsyncBlocks];
1089 XCTAssertEqual(updateCount, 0);
1092 inputView.markedTextRange = range;
1093 inputView.selectedTextRange = nil;
1094 [
self flushScheduledAsyncBlocks];
1095 XCTAssertEqual(updateCount, 1);
1097 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1099 flutterTextInputView:inputView
1100 updateEditingClient:0
1101 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1102 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1103 isEqualToString:
@"Some initial text"]) &&
1104 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1105 isEqualToString:
@"text."]) &&
1106 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1107 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1109 [
self flushScheduledAsyncBlocks];
1110 XCTAssertEqual(updateCount, 2);
1113 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1115 inputView.enableDeltaModel = YES;
1117 __block
int updateCount = 0;
1118 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1119 .andDo(^(NSInvocation* invocation) {
1123 [inputView.text setString:@"Some initial text"];
1124 [
self flushScheduledAsyncBlocks];
1125 XCTAssertEqual(updateCount, 0);
1128 inputView.markedTextRange = range;
1129 inputView.selectedTextRange = nil;
1130 [
self flushScheduledAsyncBlocks];
1131 XCTAssertEqual(updateCount, 1);
1133 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1135 flutterTextInputView:inputView
1136 updateEditingClient:0
1137 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1138 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1139 isEqualToString:
@"Some initial text"]) &&
1140 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1141 isEqualToString:
@"tex"]) &&
1142 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1143 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1145 [
self flushScheduledAsyncBlocks];
1146 XCTAssertEqual(updateCount, 2);
1149 #pragma mark - EditingState tests
1151 - (void)testUITextInputCallsUpdateEditingStateOnce {
1154 __block
int updateCount = 0;
1155 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1156 .andDo(^(NSInvocation* invocation) {
1160 [inputView insertText:@"text to insert"];
1162 XCTAssertEqual(updateCount, 1);
1164 [inputView deleteBackward];
1165 XCTAssertEqual(updateCount, 2);
1168 XCTAssertEqual(updateCount, 3);
1171 withText:@"replace text"];
1172 XCTAssertEqual(updateCount, 4);
1174 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1175 XCTAssertEqual(updateCount, 5);
1177 [inputView unmarkText];
1178 XCTAssertEqual(updateCount, 6);
1181 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1183 inputView.enableDeltaModel = YES;
1185 __block
int updateCount = 0;
1186 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1187 .andDo(^(NSInvocation* invocation) {
1191 [inputView insertText:@"text to insert"];
1192 [
self flushScheduledAsyncBlocks];
1194 XCTAssertEqual(updateCount, 1);
1196 [inputView deleteBackward];
1197 [
self flushScheduledAsyncBlocks];
1198 XCTAssertEqual(updateCount, 2);
1201 [
self flushScheduledAsyncBlocks];
1202 XCTAssertEqual(updateCount, 3);
1205 withText:@"replace text"];
1206 [
self flushScheduledAsyncBlocks];
1207 XCTAssertEqual(updateCount, 4);
1209 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1210 [
self flushScheduledAsyncBlocks];
1211 XCTAssertEqual(updateCount, 5);
1213 [inputView unmarkText];
1214 [
self flushScheduledAsyncBlocks];
1215 XCTAssertEqual(updateCount, 6);
1218 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1221 __block
int updateCount = 0;
1222 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1223 .andDo(^(NSInvocation* invocation) {
1227 [inputView.text setString:@"BEFORE"];
1228 XCTAssertEqual(updateCount, 0);
1230 inputView.markedTextRange = nil;
1231 inputView.selectedTextRange = nil;
1232 XCTAssertEqual(updateCount, 1);
1235 XCTAssertEqual(updateCount, 1);
1236 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1237 XCTAssertEqual(updateCount, 1);
1238 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1239 XCTAssertEqual(updateCount, 1);
1243 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1244 XCTAssertEqual(updateCount, 1);
1246 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1247 XCTAssertEqual(updateCount, 1);
1251 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1252 XCTAssertEqual(updateCount, 1);
1254 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1255 XCTAssertEqual(updateCount, 1);
1258 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1260 inputView.enableDeltaModel = YES;
1262 __block
int updateCount = 0;
1263 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1264 .andDo(^(NSInvocation* invocation) {
1268 [inputView.text setString:@"BEFORE"];
1269 [
self flushScheduledAsyncBlocks];
1270 XCTAssertEqual(updateCount, 0);
1272 inputView.markedTextRange = nil;
1273 inputView.selectedTextRange = nil;
1274 [
self flushScheduledAsyncBlocks];
1275 XCTAssertEqual(updateCount, 1);
1278 XCTAssertEqual(updateCount, 1);
1279 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1280 [
self flushScheduledAsyncBlocks];
1281 XCTAssertEqual(updateCount, 1);
1283 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1284 [
self flushScheduledAsyncBlocks];
1285 XCTAssertEqual(updateCount, 1);
1289 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1290 [
self flushScheduledAsyncBlocks];
1291 XCTAssertEqual(updateCount, 1);
1294 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1295 [
self flushScheduledAsyncBlocks];
1296 XCTAssertEqual(updateCount, 1);
1300 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1301 [
self flushScheduledAsyncBlocks];
1302 XCTAssertEqual(updateCount, 1);
1305 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1306 [
self flushScheduledAsyncBlocks];
1307 XCTAssertEqual(updateCount, 1);
1310 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1313 __block
int updateCount = 0;
1314 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1315 .andDo(^(NSInvocation* invocation) {
1319 [inputView unmarkText];
1321 XCTAssertEqual(updateCount, 0);
1323 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1325 XCTAssertEqual(updateCount, 1);
1327 [inputView unmarkText];
1329 XCTAssertEqual(updateCount, 2);
1332 - (void)testCanCopyPasteWithScribbleEnabled {
1333 if (@available(iOS 14.0, *)) {
1334 NSDictionary* config =
self.mutableTemplateCopy;
1335 [
self setClientId:123 configuration:config];
1336 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1342 [mockInputView insertText:@"aaaa"];
1343 [mockInputView selectAll:nil];
1345 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1346 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1347 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1348 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1350 [mockInputView copy:NULL];
1351 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1352 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1353 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1354 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1358 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1359 if (@available(iOS 14.0, *)) {
1362 __block
int updateCount = 0;
1363 OCMStub([
engine flutterTextInputView:inputView
1364 updateEditingClient:0
1365 withState:[OCMArg isNotNil]])
1366 .andDo(^(NSInvocation* invocation) {
1370 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1372 XCTAssertEqual(updateCount, 1);
1374 UIScribbleInteraction* scribbleInteraction =
1375 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1377 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1378 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1380 XCTAssertEqual(updateCount, 1);
1382 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1383 [inputView resetScribbleInteractionStatusIfEnding];
1384 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1386 XCTAssertEqual(updateCount, 2);
1388 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1389 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1392 XCTAssertEqual(updateCount, 2);
1394 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1395 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1398 XCTAssertEqual(updateCount, 2);
1400 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1401 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1403 XCTAssertEqual(updateCount, 3);
1407 - (void)testUpdateEditingClientNegativeSelection {
1410 [inputView.text setString:@"SELECTION"];
1411 inputView.markedTextRange = nil;
1412 inputView.selectedTextRange = nil;
1414 [inputView setTextInputState:@{
1415 @"text" : @"SELECTION",
1416 @"selectionBase" : @-1,
1417 @"selectionExtent" : @-1
1419 [inputView updateEditingState];
1420 OCMVerify([
engine flutterTextInputView:inputView
1421 updateEditingClient:0
1422 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1423 return ([state[
@"selectionBase"] intValue]) == 0 &&
1424 ([state[
@"selectionExtent"] intValue] == 0);
1429 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1430 [inputView updateEditingState];
1431 OCMVerify([
engine flutterTextInputView:inputView
1432 updateEditingClient:0
1433 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1434 return ([state[
@"selectionBase"] intValue]) == 0 &&
1435 ([state[
@"selectionExtent"] intValue] == 0);
1439 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1440 [inputView updateEditingState];
1441 OCMVerify([
engine flutterTextInputView:inputView
1442 updateEditingClient:0
1443 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1444 return ([state[
@"selectionBase"] intValue]) == 0 &&
1445 ([state[
@"selectionExtent"] intValue] == 0);
1449 - (void)testUpdateEditingClientSelectionClamping {
1453 [inputView.text setString:@"SELECTION"];
1454 inputView.markedTextRange = nil;
1455 inputView.selectedTextRange = nil;
1458 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1459 [inputView updateEditingState];
1460 OCMVerify([
engine flutterTextInputView:inputView
1461 updateEditingClient:0
1462 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1463 return ([state[
@"selectionBase"] intValue]) == 0 &&
1464 ([state[
@"selectionExtent"] intValue] == 0);
1468 [inputView setTextInputState:@{
1469 @"text" : @"SELECTION",
1470 @"selectionBase" : @0,
1471 @"selectionExtent" : @9999
1473 [inputView updateEditingState];
1475 OCMVerify([
engine flutterTextInputView:inputView
1476 updateEditingClient:0
1477 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1478 return ([state[
@"selectionBase"] intValue]) == 0 &&
1479 ([state[
@"selectionExtent"] intValue] == 9);
1484 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1485 [inputView updateEditingState];
1486 OCMVerify([
engine flutterTextInputView:inputView
1487 updateEditingClient:0
1488 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1489 return ([state[
@"selectionBase"] intValue]) == 0 &&
1490 ([state[
@"selectionExtent"] intValue] == 1);
1494 [inputView setTextInputState:@{
1495 @"text" : @"SELECTION",
1496 @"selectionBase" : @9999,
1497 @"selectionExtent" : @9999
1499 [inputView updateEditingState];
1500 OCMVerify([
engine flutterTextInputView:inputView
1501 updateEditingClient:0
1502 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1503 return ([state[
@"selectionBase"] intValue]) == 9 &&
1504 ([state[
@"selectionExtent"] intValue] == 9);
1508 - (void)testInputViewsHasNonNilInputDelegate {
1509 if (@available(iOS 13.0, *)) {
1511 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1513 [inputView setTextInputClient:123];
1514 [inputView reloadInputViews];
1515 [inputView becomeFirstResponder];
1516 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1517 inputView.inputDelegate = nil;
1520 [mockInputView setTextInputState:@{
1521 @"text" : @"COMPOSING",
1522 @"composingBase" : @1,
1523 @"composingExtent" : @3
1525 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1526 [inputView removeFromSuperview];
1530 - (void)testInputViewsDoNotHaveUITextInteractions {
1531 if (@available(iOS 13.0, *)) {
1533 BOOL hasTextInteraction = NO;
1534 for (
id interaction in inputView.interactions) {
1535 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1536 if (hasTextInteraction) {
1540 XCTAssertFalse(hasTextInteraction);
1544 #pragma mark - UITextInput methods - Tests
1546 - (void)testUpdateFirstRectForRange {
1547 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1553 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1558 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1559 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1563 NSArray* affineMatrix = @[
1564 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1565 @(-6.0), @(3.0), @(9.0), @(1.0)
1569 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1571 [inputView setEditableTransform:yOffsetMatrix];
1573 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1576 CGRect testRect = CGRectMake(0, 0, 100, 100);
1577 [inputView setMarkedRect:testRect];
1579 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1580 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1582 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1585 [inputView setEditableTransform:zeroMatrix];
1587 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1588 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1591 [inputView setEditableTransform:yOffsetMatrix];
1592 [inputView setMarkedRect:testRect];
1593 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1596 [inputView setMarkedRect:kInvalidFirstRect];
1598 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1599 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1602 [inputView setEditableTransform:affineMatrix];
1603 [inputView setMarkedRect:testRect];
1605 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1607 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1608 const CGPoint offset = CGPointMake(113, 119);
1609 CGRect currentFrame = inputView.frame;
1610 currentFrame.origin = offset;
1611 inputView.frame = currentFrame;
1614 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1615 [inputView firstRectForRange:range]));
1618 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1620 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1625 [inputView setSelectionRects:@[
1634 if (@available(iOS 17, *)) {
1635 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1636 [inputView firstRectForRange:multiRectRange]));
1638 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1639 [inputView firstRectForRange:multiRectRange]));
1643 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1645 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1647 [inputView setSelectionRects:@[
1654 if (@available(iOS 17, *)) {
1655 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1656 [inputView firstRectForRange:singleRectRange]));
1658 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1663 if (@available(iOS 17, *)) {
1664 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1665 [inputView firstRectForRange:multiRectRange]));
1667 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1670 [inputView setTextInputState:@{@"text" : @"COM"}];
1672 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1675 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1677 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1679 [inputView setSelectionRects:@[
1686 if (@available(iOS 17, *)) {
1687 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1688 [inputView firstRectForRange:singleRectRange]));
1690 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1694 if (@available(iOS 17, *)) {
1695 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1696 [inputView firstRectForRange:multiRectRange]));
1698 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1701 [inputView setTextInputState:@{@"text" : @"COM"}];
1703 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1706 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1708 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1710 [inputView setSelectionRects:@[
1721 if (@available(iOS 17, *)) {
1722 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1723 [inputView firstRectForRange:singleRectRange]));
1725 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1730 if (@available(iOS 17, *)) {
1731 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1732 [inputView firstRectForRange:multiRectRange]));
1734 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1738 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1740 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1742 [inputView setSelectionRects:@[
1753 if (@available(iOS 17, *)) {
1754 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1755 [inputView firstRectForRange:singleRectRange]));
1757 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1761 if (@available(iOS 17, *)) {
1762 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1763 [inputView firstRectForRange:multiRectRange]));
1765 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1769 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1771 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1773 [inputView setSelectionRects:@[
1784 if (@available(iOS 17, *)) {
1785 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1786 [inputView firstRectForRange:multiRectRange]));
1788 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1792 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1794 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1796 [inputView setSelectionRects:@[
1807 if (@available(iOS 17, *)) {
1808 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1809 [inputView firstRectForRange:multiRectRange]));
1811 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1815 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1817 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1819 [inputView setSelectionRects:@[
1830 if (@available(iOS 17, *)) {
1831 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1832 [inputView firstRectForRange:multiRectRange]));
1834 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1838 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1840 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1842 [inputView setSelectionRects:@[
1853 if (@available(iOS 17, *)) {
1854 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1855 [inputView firstRectForRange:multiRectRange]));
1857 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1861 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1863 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1865 [inputView setSelectionRects:@[
1876 if (@available(iOS 17, *)) {
1877 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1878 [inputView firstRectForRange:multiRectRange]));
1880 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1884 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1886 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1888 [inputView setSelectionRects:@[
1899 if (@available(iOS 17, *)) {
1900 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1901 [inputView firstRectForRange:multiRectRange]));
1903 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1907 - (void)testClosestPositionToPoint {
1909 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1912 [inputView setSelectionRects:@[
1917 CGPoint point = CGPointMake(150, 150);
1918 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1919 XCTAssertEqual(UITextStorageDirectionBackward,
1924 [inputView setSelectionRects:@[
1931 point = CGPointMake(125, 150);
1932 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1933 XCTAssertEqual(UITextStorageDirectionForward,
1938 [inputView setSelectionRects:@[
1945 point = CGPointMake(125, 201);
1946 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1947 XCTAssertEqual(UITextStorageDirectionBackward,
1951 [inputView setSelectionRects:@[
1957 point = CGPointMake(125, 250);
1958 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1959 XCTAssertEqual(UITextStorageDirectionBackward,
1963 [inputView setSelectionRects:@[
1968 point = CGPointMake(110, 50);
1969 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1970 XCTAssertEqual(UITextStorageDirectionForward,
1975 [inputView beginFloatingCursorAtPoint:CGPointZero];
1976 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1977 XCTAssertEqual(UITextStorageDirectionForward,
1979 [inputView endFloatingCursor];
1982 - (void)testClosestPositionToPointRTL {
1984 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1986 [inputView setSelectionRects:@[
2002 XCTAssertEqual(0U, position.
index);
2003 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2005 XCTAssertEqual(1U, position.
index);
2006 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2008 XCTAssertEqual(1U, position.
index);
2009 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2011 XCTAssertEqual(2U, position.
index);
2012 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2014 XCTAssertEqual(2U, position.
index);
2015 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2017 XCTAssertEqual(3U, position.
index);
2018 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2020 XCTAssertEqual(3U, position.
index);
2021 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2024 - (void)testSelectionRectsForRange {
2026 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2028 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2029 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2030 [inputView setSelectionRects:@[
2039 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2040 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2041 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2045 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2046 XCTAssertTrue(CGRectEqualToRect(
2047 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2048 [inputView selectionRectsForRange:range][0].rect));
2051 - (void)testClosestPositionToPointWithinRange {
2053 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2056 [inputView setSelectionRects:@[
2063 CGPoint point = CGPointMake(125, 150);
2066 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2068 UITextStorageDirectionForward,
2069 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2072 [inputView setSelectionRects:@[
2079 point = CGPointMake(125, 150);
2082 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2084 UITextStorageDirectionForward,
2085 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2088 - (void)testClosestPositionToPointWithPartialSelectionRects {
2090 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2097 XCTAssertTrue(CGRectEqualToRect(
2100 affinity:UITextStorageDirectionForward]],
2101 CGRectMake(100, 0, 0, 100)));
2104 XCTAssertTrue(CGRectEqualToRect(
2107 affinity:UITextStorageDirectionForward]],
2111 #pragma mark - Floating Cursor - Tests
2113 - (void)testFloatingCursorDoesNotThrow {
2116 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2117 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2118 [inputView endFloatingCursor];
2119 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2120 [inputView endFloatingCursor];
2123 - (void)testFloatingCursor {
2125 [inputView setTextInputState:@{
2127 @"selectionBase" : @1,
2128 @"selectionExtent" : @1,
2139 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2142 XCTAssertTrue(CGRectEqualToRect(
2145 affinity:UITextStorageDirectionForward]],
2146 CGRectMake(0, 0, 0, 100)));
2149 XCTAssertTrue(CGRectEqualToRect(
2152 affinity:UITextStorageDirectionForward]],
2153 CGRectMake(100, 100, 0, 100)));
2154 XCTAssertTrue(CGRectEqualToRect(
2157 affinity:UITextStorageDirectionForward]],
2158 CGRectMake(200, 200, 0, 100)));
2159 XCTAssertTrue(CGRectEqualToRect(
2162 affinity:UITextStorageDirectionForward]],
2163 CGRectMake(300, 300, 0, 100)));
2166 XCTAssertTrue(CGRectEqualToRect(
2169 affinity:UITextStorageDirectionForward]],
2170 CGRectMake(400, 300, 0, 100)));
2172 XCTAssertTrue(CGRectEqualToRect(
2175 affinity:UITextStorageDirectionForward]],
2179 [inputView setTextInputState:@{
2181 @"selectionBase" : @2,
2182 @"selectionExtent" : @2,
2185 XCTAssertTrue(CGRectEqualToRect(
2188 affinity:UITextStorageDirectionBackward]],
2189 CGRectMake(0, 0, 0, 100)));
2192 XCTAssertTrue(CGRectEqualToRect(
2195 affinity:UITextStorageDirectionBackward]],
2196 CGRectMake(100, 0, 0, 100)));
2197 XCTAssertTrue(CGRectEqualToRect(
2200 affinity:UITextStorageDirectionBackward]],
2201 CGRectMake(200, 100, 0, 100)));
2202 XCTAssertTrue(CGRectEqualToRect(
2205 affinity:UITextStorageDirectionBackward]],
2206 CGRectMake(300, 200, 0, 100)));
2207 XCTAssertTrue(CGRectEqualToRect(
2210 affinity:UITextStorageDirectionBackward]],
2211 CGRectMake(400, 300, 0, 100)));
2213 XCTAssertTrue(CGRectEqualToRect(
2216 affinity:UITextStorageDirectionBackward]],
2221 CGRect initialBounds = inputView.bounds;
2222 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2223 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2224 OCMVerify([
engine flutterTextInputView:inputView
2225 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2227 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2228 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2229 ([state[
@"Y"] isEqualToNumber:@(0)]);
2232 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2233 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2234 OCMVerify([
engine flutterTextInputView:inputView
2235 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2237 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2238 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2239 ([state[
@"Y"] isEqualToNumber:@(333)]);
2242 [inputView endFloatingCursor];
2243 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2244 OCMVerify([
engine flutterTextInputView:inputView
2245 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2247 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2248 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2249 ([state[
@"Y"] isEqualToNumber:@(0)]);
2253 #pragma mark - UIKeyInput Overrides - Tests
2255 - (void)testInsertTextAddsPlaceholderSelectionRects {
2258 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2268 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2271 [inputView insertText:@"in"];
2299 #pragma mark - Autofill - Utilities
2301 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2304 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2305 @"keyboardAppearance" :
@"Brightness.light",
2306 @"obscureText" : @YES,
2307 @"inputAction" :
@"TextInputAction.unspecified",
2308 @"smartDashesType" :
@"0",
2309 @"smartQuotesType" :
@"0",
2310 @"autocorrect" : @YES
2314 return [_passwordTemplate mutableCopy];
2318 return [
self.installedInputViews
2319 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2322 - (void)commitAutofillContextAndVerify {
2326 [textInputPlugin handleMethodCall:methodCall
2327 result:^(id _Nullable result){
2330 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2335 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2339 #pragma mark - Autofill - Tests
2341 - (void)testDisablingAutofillOnInputClient {
2342 NSDictionary* config =
self.mutableTemplateCopy;
2343 [config setValue:@"YES" forKey:@"obscureText"];
2345 [
self setClientId:123 configuration:config];
2348 XCTAssertEqualObjects(inputView.textContentType,
@"");
2351 - (void)testAutofillEnabledByDefault {
2352 NSDictionary* config =
self.mutableTemplateCopy;
2353 [config setValue:@"NO" forKey:@"obscureText"];
2354 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2355 forKey:@"autofill"];
2357 [
self setClientId:123 configuration:config];
2360 XCTAssertNil(inputView.textContentType);
2363 - (void)testAutofillContext {
2364 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2367 @"uniqueIdentifier" : @"field1",
2368 @"hints" : @[ @"hint1" ],
2369 @"editingValue" : @{@"text" : @""}
2371 forKey:@"autofill"];
2373 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2375 @"uniqueIdentifier" : @"field2",
2376 @"hints" : @[ @"hint2" ],
2377 @"editingValue" : @{@"text" : @""}
2379 forKey:@"autofill"];
2381 NSMutableDictionary* config = [field1 mutableCopy];
2382 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2384 [
self setClientId:123 configuration:config];
2385 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2389 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2390 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2392 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2395 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2397 @"uniqueIdentifier" : @"field3",
2398 @"hints" : @[ @"hint3" ],
2399 @"editingValue" : @{@"text" : @""}
2401 forKey:@"autofill"];
2405 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2407 [
self setClientId:123 configuration:config];
2409 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2412 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2413 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2415 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2418 for (NSString* key in oldContext.allKeys) {
2419 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2423 config =
self.mutablePasswordTemplateCopy;
2426 [
self setClientId:124 configuration:config];
2427 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2429 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2432 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2433 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2436 for (NSString* key in oldContext.allKeys) {
2437 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2441 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2445 [
self setClientId:200 configuration:config];
2448 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2451 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2452 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2455 for (NSString* key in oldContext.allKeys) {
2456 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2459 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2462 - (void)testCommitAutofillContext {
2463 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2465 @"uniqueIdentifier" : @"field1",
2466 @"hints" : @[ @"hint1" ],
2467 @"editingValue" : @{@"text" : @""}
2469 forKey:@"autofill"];
2471 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2473 @"uniqueIdentifier" : @"field2",
2474 @"hints" : @[ @"hint2" ],
2475 @"editingValue" : @{@"text" : @""}
2477 forKey:@"autofill"];
2479 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2481 @"uniqueIdentifier" : @"field3",
2482 @"hints" : @[ @"hint3" ],
2483 @"editingValue" : @{@"text" : @""}
2485 forKey:@"autofill"];
2487 NSMutableDictionary* config = [field1 mutableCopy];
2488 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2490 [
self setClientId:123 configuration:config];
2491 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2493 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2495 [
self commitAutofillContextAndVerify];
2496 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2499 [
self setClientId:123 configuration:config];
2501 [
self setClientId:124 configuration:field3];
2502 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2504 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2505 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2508 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2510 [
self commitAutofillContextAndVerify];
2511 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2514 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2516 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2520 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2521 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2523 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2525 [
self commitAutofillContextAndVerify];
2526 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2529 - (void)testAutofillInputViews {
2530 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2532 @"uniqueIdentifier" : @"field1",
2533 @"hints" : @[ @"hint1" ],
2534 @"editingValue" : @{@"text" : @""}
2536 forKey:@"autofill"];
2538 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2540 @"uniqueIdentifier" : @"field2",
2541 @"hints" : @[ @"hint2" ],
2542 @"editingValue" : @{@"text" : @""}
2544 forKey:@"autofill"];
2546 NSMutableDictionary* config = [field1 mutableCopy];
2547 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2549 [
self setClientId:123 configuration:config];
2550 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2553 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2556 XCTAssertEqual(inputFields.count, 2ul);
2557 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2562 withText:@"Autofilled!"];
2563 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2566 OCMVerify([
engine flutterTextInputView:inactiveView
2567 updateEditingClient:0
2568 withState:[OCMArg isNotNil]
2569 withTag:
@"field2"]);
2572 - (void)testPasswordAutofillHack {
2573 NSDictionary* config =
self.mutableTemplateCopy;
2574 [config setValue:@"YES" forKey:@"obscureText"];
2575 [
self setClientId:123 configuration:config];
2578 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2582 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2585 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2588 - (void)testClearAutofillContextClearsSelection {
2589 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2590 NSDictionary* editingValue = @{
2591 @"text" :
@"REGULAR_TEXT_FIELD",
2592 @"composingBase" : @0,
2593 @"composingExtent" : @3,
2594 @"selectionBase" : @1,
2595 @"selectionExtent" : @4
2597 [regularField setValue:@{
2598 @"uniqueIdentifier" : @"field2",
2599 @"hints" : @[ @"hint2" ],
2600 @"editingValue" : editingValue,
2602 forKey:@"autofill"];
2603 [regularField addEntriesFromDictionary:editingValue];
2604 [
self setClientId:123 configuration:regularField];
2605 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2606 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2609 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2611 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2615 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2616 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2618 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2620 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2621 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2624 XCTAssert([oldInputView.text isEqualToString:
@""]);
2626 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2629 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2631 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2632 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2634 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2637 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2638 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2640 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2642 [
self commitAutofillContextAndVerify];
2645 - (void)testScribbleSetSelectionRects {
2646 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2647 NSDictionary* editingValue = @{
2648 @"text" :
@"REGULAR_TEXT_FIELD",
2649 @"composingBase" : @0,
2650 @"composingExtent" : @3,
2651 @"selectionBase" : @1,
2652 @"selectionExtent" : @4
2654 [regularField setValue:@{
2655 @"uniqueIdentifier" : @"field1",
2656 @"hints" : @[ @"hint2" ],
2657 @"editingValue" : editingValue,
2659 forKey:@"autofill"];
2660 [regularField addEntriesFromDictionary:editingValue];
2661 [
self setClientId:123 configuration:regularField];
2662 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2663 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2665 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2666 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2670 [textInputPlugin handleMethodCall:methodCall
2671 result:^(id _Nullable result){
2674 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2677 - (void)testDecommissionedViewAreNotReusedByAutofill {
2679 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2680 [configuration setValue:@{
2681 @"uniqueIdentifier" : @"field1",
2682 @"hints" : @[ UITextContentTypePassword ],
2683 @"editingValue" : @{@"text" : @""}
2685 forKey:@"autofill"];
2686 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2688 [
self setClientId:123 configuration:configuration];
2690 [
self setTextInputHide];
2693 [
self setClientId:124 configuration:configuration];
2697 XCTAssertNotNil(previousActiveView);
2701 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2708 #pragma mark - Accessibility - Tests
2710 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2711 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2714 [
self setTextInputShow];
2716 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2719 XCTAssertEqual([inputFields count], 1u);
2722 [
self setTextInputHide];
2724 inputFields =
self.installedInputViews;
2727 XCTAssertEqual([inputFields count], 0u);
2730 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2733 UIView* container = [[UIView alloc] init];
2734 UIAccessibilityElement* backing =
2735 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2736 inputView.backingTextInputAccessibilityObject = backing;
2739 [inputView accessibilityElementDidBecomeFocused];
2745 - (void)testFlutterTokenizerCanParseLines {
2747 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2750 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2751 XCTAssertEqual(range.
range.location, 0u);
2752 XCTAssertEqual(range.
range.length, 0u);
2754 [inputView insertText:@"how are you\nI am fine, Thank you"];
2756 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2757 XCTAssertEqual(range.
range.location, 0u);
2758 XCTAssertEqual(range.
range.length, 11u);
2760 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2761 XCTAssertEqual(range.
range.location, 0u);
2762 XCTAssertEqual(range.
range.length, 11u);
2764 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2765 XCTAssertEqual(range.
range.location, 0u);
2766 XCTAssertEqual(range.
range.length, 11u);
2768 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2769 XCTAssertEqual(range.
range.location, 12u);
2770 XCTAssertEqual(range.
range.length, 20u);
2772 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2773 XCTAssertEqual(range.
range.location, 12u);
2774 XCTAssertEqual(range.
range.length, 20u);
2776 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2777 XCTAssertEqual(range.
range.location, 12u);
2778 XCTAssertEqual(range.
range.length, 20u);
2781 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2783 [inputView insertText:@"0123456789\n012345"];
2784 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2787 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2788 withGranularity:UITextGranularityLine
2789 inDirection:UITextStorageDirectionBackward];
2790 XCTAssertEqual(range.
range.location, 11u);
2791 XCTAssertEqual(range.
range.length, 6u);
2794 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2796 [inputView insertText:@"0123456789\n012345"];
2797 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2800 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2801 withGranularity:UITextGranularityLine
2802 inDirection:UITextStorageDirectionForward];
2803 if (@available(iOS 17.0, *)) {
2804 XCTAssertNil(range);
2806 XCTAssertEqual(range.
range.location, 11u);
2807 XCTAssertEqual(range.
range.length, 6u);
2811 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2813 [inputView insertText:@"0123456789\n012345"];
2814 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2819 withGranularity:UITextGranularityLine
2820 inDirection:UITextStorageDirectionForward];
2821 if (@available(iOS 17.0, *)) {
2822 XCTAssertNil(range);
2824 XCTAssertEqual(range.
range.location, 0u);
2825 XCTAssertEqual(range.
range.length, 0u);
2829 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2834 __weak UIView* activeView;
2839 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2842 result:^(id _Nullable result){
2848 result:^(id _Nullable result){
2850 XCTAssertNotNil(activeView);
2853 XCTAssertNotNil(activeView);
2856 - (void)testFlutterTextInputPluginHostViewNilCrash {
2859 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2862 - (void)testFlutterTextInputPluginHostViewNotNil {
2868 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2871 - (void)testSetPlatformViewClient {
2878 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2880 result:^(id _Nullable result){
2883 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2886 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2888 result:^(id _Nullable result){
2890 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2893 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
2894 if (@available(iOS 16.0, *)) {
2896 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2897 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
2898 @"editMenuInteraction setup delegate correctly");
2902 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
2903 if (@available(iOS 16.0, *)) {
2907 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
2911 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
2912 if (@available(iOS 16.0, *)) {
2917 [myViewController loadView];
2920 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2922 result:^(id _Nullable result){
2928 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2930 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2931 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2933 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2934 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2935 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2936 .andDo(^(NSInvocation* invocation) {
2938 [invocation retainArguments];
2939 UIEditMenuConfiguration* config;
2940 [invocation getArgument:&config atIndex:2];
2941 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
2942 @"UIEditMenuConfiguration must use automatic arrow direction.");
2943 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
2944 @"UIEditMenuConfiguration must have the correct point.");
2945 [expectation fulfill];
2948 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2949 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
2951 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2952 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2953 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2957 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
2958 if (@available(iOS 16.0, *)) {
2963 [myViewController loadView];
2967 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2969 result:^(id _Nullable result){
2975 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2977 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2978 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2980 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2981 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2982 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2983 .andDo(^(NSInvocation* invocation) {
2984 [expectation fulfill];
2987 myInputView.frame = CGRectMake(10, 20, 30, 40);
2988 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2989 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
2991 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2992 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2993 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2996 [myInputView editMenuInteraction:mockInteraction
2997 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
2999 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3000 @"targetRectForConfiguration must return the correct target rect.");
3004 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3006 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3008 [inputView setTextInputClient:123];
3009 [inputView reloadInputViews];
3010 [inputView becomeFirstResponder];
3011 XCTAssert(inputView.isFirstResponder);
3013 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3014 [NSNotificationCenter.defaultCenter
3015 postNotificationName:UIKeyboardWillShowNotification
3017 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3021 [textInputPlugin handleMethodCall:onPointerMoveCall
3022 result:^(id _Nullable result){
3024 XCTAssertFalse(inputView.isFirstResponder);
3028 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3029 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3030 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3031 UIScene* scene = scenes.anyObject;
3032 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3033 UIWindowScene* windowScene = (UIWindowScene*)scene;
3034 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3035 UIWindow* window = windowScene.windows[0];
3036 [window addSubview:viewController.view];
3038 [viewController loadView];
3041 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3043 [inputView setTextInputClient:123];
3044 [inputView reloadInputViews];
3045 [inputView becomeFirstResponder];
3048 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3049 [subView removeFromSuperview];
3053 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3054 [NSNotificationCenter.defaultCenter
3055 postNotificationName:UIKeyboardWillShowNotification
3057 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3061 [textInputPlugin handleMethodCall:onPointerMoveCall
3062 result:^(id _Nullable result){
3065 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3066 [subView removeFromSuperview];
3071 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3072 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3073 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3074 UIScene* scene = scenes.anyObject;
3075 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3076 UIWindowScene* windowScene = (UIWindowScene*)scene;
3077 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3078 UIWindow* window = windowScene.windows[0];
3079 [window addSubview:viewController.view];
3081 [viewController loadView];
3084 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3086 [inputView setTextInputClient:123];
3087 [inputView reloadInputViews];
3088 [inputView becomeFirstResponder];
3090 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3091 [NSNotificationCenter.defaultCenter
3092 postNotificationName:UIKeyboardWillShowNotification
3094 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3098 [textInputPlugin handleMethodCall:onPointerMoveCall
3099 result:^(id _Nullable result){
3103 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3108 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3109 result:^(id _Nullable result){
3113 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3115 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3116 [subView removeFromSuperview];
3121 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3122 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3123 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3124 UIScene* scene = scenes.anyObject;
3125 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3126 UIWindowScene* windowScene = (UIWindowScene*)scene;
3127 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3128 UIWindow* window = windowScene.windows[0];
3129 [window addSubview:viewController.view];
3131 [viewController loadView];
3134 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3136 [inputView setTextInputClient:123];
3137 [inputView reloadInputViews];
3138 [inputView becomeFirstResponder];
3140 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3141 [NSNotificationCenter.defaultCenter
3142 postNotificationName:UIKeyboardWillShowNotification
3144 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3148 [textInputPlugin handleMethodCall:onPointerMoveCall
3149 result:^(id _Nullable result){
3152 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3157 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3158 result:^(id _Nullable result){
3161 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3166 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3167 result:^(id _Nullable result){
3170 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3171 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3172 [subView removeFromSuperview];
3177 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3179 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3180 [inputView setTextInputClient:123];
3181 [inputView reloadInputViews];
3182 [inputView becomeFirstResponder];
3184 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3185 XCTAssertEqualObjects(inputView, firstResponder);
3189 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3196 [subInputView addSubview:subFirstResponderInputView];
3197 [inputView addSubview:subInputView];
3198 [inputView addSubview:otherSubInputView];
3199 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3200 [inputView setTextInputClient:123];
3201 [inputView reloadInputViews];
3202 [subInputView setTextInputClient:123];
3203 [subInputView reloadInputViews];
3204 [otherSubInputView setTextInputClient:123];
3205 [otherSubInputView reloadInputViews];
3206 [subFirstResponderInputView setTextInputClient:123];
3207 [subFirstResponderInputView reloadInputViews];
3208 [subFirstResponderInputView becomeFirstResponder];
3210 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3211 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3215 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3217 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3218 [inputView setTextInputClient:123];
3219 [inputView reloadInputViews];
3221 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3222 XCTAssertNil(firstResponder);
3226 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3227 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3228 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3229 UIScene* scene = scenes.anyObject;
3230 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3231 UIWindowScene* windowScene = (UIWindowScene*)scene;
3232 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3233 UIWindow* window = windowScene.windows[0];
3234 [window addSubview:viewController.view];
3236 [viewController loadView];
3238 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3239 initWithDescription:
3240 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3241 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3242 .andDo(^(NSInvocation* invocation) {
3243 [expectation fulfill];
3245 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3246 [NSNotificationCenter.defaultCenter
3247 postNotificationName:UIKeyboardWillShowNotification
3249 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3253 [textInputPlugin handleMethodCall:initialMoveCall
3254 result:^(id _Nullable result){
3259 [textInputPlugin handleMethodCall:subsequentMoveCall
3260 result:^(id _Nullable result){
3266 [textInputPlugin handleMethodCall:pointerUpCall
3267 result:^(id _Nullable result){
3270 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3274 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3275 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3276 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3277 UIScene* scene = scenes.anyObject;
3278 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3279 UIWindowScene* windowScene = (UIWindowScene*)scene;
3280 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3281 UIWindow* window = windowScene.windows[0];
3282 [window addSubview:viewController.view];
3284 [viewController loadView];
3286 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3287 [NSNotificationCenter.defaultCenter
3288 postNotificationName:UIKeyboardWillShowNotification
3290 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3294 [textInputPlugin handleMethodCall:initialMoveCall
3295 result:^(id _Nullable result){
3300 [textInputPlugin handleMethodCall:subsequentMoveCall
3301 result:^(id _Nullable result){
3307 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3308 result:^(id _Nullable result){
3314 [textInputPlugin handleMethodCall:pointerUpCall
3315 result:^(id _Nullable result){
3317 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3318 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3320 XCTNSPredicateExpectation* expectation =
3321 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3322 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3326 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3327 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3328 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3329 UIScene* scene = scenes.anyObject;
3330 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3331 UIWindowScene* windowScene = (UIWindowScene*)scene;
3332 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3333 UIWindow* window = windowScene.windows[0];
3334 [window addSubview:viewController.view];
3336 [viewController loadView];
3339 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3341 [inputView setTextInputClient:123];
3342 [inputView reloadInputViews];
3343 [inputView becomeFirstResponder];
3345 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3346 [NSNotificationCenter.defaultCenter
3347 postNotificationName:UIKeyboardWillShowNotification
3349 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3353 [textInputPlugin handleMethodCall:initialMoveCall
3354 result:^(id _Nullable result){
3359 [textInputPlugin handleMethodCall:subsequentMoveCall
3360 result:^(id _Nullable result){
3366 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3367 result:^(id _Nullable result){
3373 [textInputPlugin handleMethodCall:pointerUpCall
3374 result:^(id _Nullable result){
3376 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3377 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3379 XCTNSPredicateExpectation* expectation =
3380 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3381 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3385 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3386 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3387 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3388 UIScene* scene = scenes.anyObject;
3389 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3390 UIWindowScene* windowScene = (UIWindowScene*)scene;
3391 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3392 UIWindow* window = windowScene.windows[0];
3393 [window addSubview:viewController.view];
3395 [viewController loadView];
3397 XCTestExpectation* expectation =
3398 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3399 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3400 [NSNotificationCenter.defaultCenter
3401 postNotificationName:UIKeyboardWillShowNotification
3403 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3407 [textInputPlugin handleMethodCall:initialMoveCall
3408 result:^(id _Nullable result){
3413 [textInputPlugin handleMethodCall:subsequentMoveCall
3414 result:^(id _Nullable result){
3419 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3420 result:^(id _Nullable result){
3427 handleMethodCall:pointerUpCall
3428 result:^(id _Nullable result) {
3429 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3430 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3431 keyboardFrame.origin.y);
3432 [expectation fulfill];
3437 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3438 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3439 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3440 UIScene* scene = scenes.anyObject;
3441 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3442 UIWindowScene* windowScene = (UIWindowScene*)scene;
3443 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3444 UIWindow* window = windowScene.windows[0];
3445 [window addSubview:viewController.view];
3447 [viewController loadView];
3449 XCTestExpectation* expectation =
3450 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3451 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3452 [NSNotificationCenter.defaultCenter
3453 postNotificationName:UIKeyboardWillShowNotification
3455 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3459 [textInputPlugin handleMethodCall:initialMoveCall
3460 result:^(id _Nullable result){
3465 [textInputPlugin handleMethodCall:subsequentMoveCall
3466 result:^(id _Nullable result){
3473 handleMethodCall:pointerUpCall
3474 result:^(id _Nullable result) {
3475 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3476 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3477 [expectation fulfill];
3481 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3482 [UIView setAnimationsEnabled:YES];
3483 [textInputPlugin showKeyboardAndRemoveScreenshot];
3485 UIView.areAnimationsEnabled,
3486 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3489 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3490 [UIView setAnimationsEnabled:YES];
3491 [textInputPlugin showKeyboardAndRemoveScreenshot];
3493 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3495 return UIView.areAnimationsEnabled;
3497 XCTNSPredicateExpectation* expectation =
3498 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3499 [
self waitForExpectations:@[ expectation ] timeout:10.0];