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 NSAssert(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 NSAssert(!weakFlutterEngine,
@"flutter engine must be nil");
258 NSAssert(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)testVisiblePasswordUseAlphanumeric {
311 NSDictionary* config =
self.mutableTemplateCopy;
312 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
313 [
self setClientId:123 configuration:config];
316 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
321 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
324 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
325 NSDictionary* config =
self.mutableTemplateCopy;
326 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
327 [
self setClientId:123 configuration:config];
332 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
333 [
self setClientId:124 configuration:config];
338 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
342 if (@available(iOS 17.0, *)) {
344 OCMVerify(never(), [
engine flutterTextInputView:inputView
345 showAutocorrectionPromptRectForStart:0
349 OCMVerify([
engine flutterTextInputView:inputView
350 showAutocorrectionPromptRectForStart:0
356 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
358 __block
int updateCount = 0;
359 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
360 .andDo(^(NSInvocation* invocation) {
364 [inputView.text setString:@"Some initial text"];
365 XCTAssertEqual(updateCount, 0);
368 [inputView setSelectedTextRange:textRange];
369 XCTAssertEqual(updateCount, 1);
372 NSDictionary* config =
self.mutableTemplateCopy;
373 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
374 [config setValue:@(NO) forKey:@"obscureText"];
375 [config setValue:@(NO) forKey:@"enableDeltaModel"];
376 [inputView configureWithDictionary:config];
379 [inputView setSelectedTextRange:textRange];
381 XCTAssertEqual(updateCount, 1);
384 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
386 if (@available(iOS 17.0, *)) {
390 if (@available(iOS 14.0, *)) {
393 __block
int callCount = 0;
394 OCMStub([
engine flutterTextInputView:inputView
395 showAutocorrectionPromptRectForStart:0
398 .andDo(^(NSInvocation* invocation) {
404 XCTAssertEqual(callCount, 1);
406 UIScribbleInteraction* scribbleInteraction =
407 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
409 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
413 XCTAssertEqual(callCount, 1);
415 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
416 [inputView resetScribbleInteractionStatusIfEnding];
419 XCTAssertEqual(callCount, 2);
421 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
425 XCTAssertEqual(callCount, 2);
427 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
431 XCTAssertEqual(callCount, 2);
433 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
436 XCTAssertEqual(callCount, 3);
440 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
446 arguments:@[ @(123),
self.mutableTemplateCopy ]];
448 result:^(id _Nullable result){
455 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
459 arguments:@{@"transform" : yOffsetMatrix}];
461 result:^(id _Nullable result){
464 if (@available(iOS 17, *)) {
465 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
466 @"The input hider should overlap with the text on and after iOS 17");
469 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
470 @"The input hider should be on the origin of screen on and before iOS 16.");
474 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
480 toPosition:toPosition];
481 NSRange range = flutterRange.
range;
483 XCTAssertEqual(range.location, 0ul);
484 XCTAssertEqual(range.length, 2ul);
487 - (void)testTextInRange {
488 NSDictionary* config =
self.mutableTemplateCopy;
489 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
490 [
self setClientId:123 configuration:config];
491 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
494 [inputView insertText:@"test"];
497 NSString* substring = [inputView textInRange:range];
498 XCTAssertEqual(substring.length, 4ul);
501 substring = [inputView textInRange:range];
502 XCTAssertEqual(substring.length, 0ul);
505 - (void)testStandardEditActions {
506 NSDictionary* config =
self.mutableTemplateCopy;
507 [
self setClientId:123 configuration:config];
508 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
511 [inputView insertText:@"aaaa"];
512 [inputView selectAll:nil];
514 [inputView insertText:@"bbbb"];
515 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
516 [inputView paste:nil];
517 [inputView selectAll:nil];
518 [inputView copy:nil];
519 [inputView paste:nil];
520 [inputView selectAll:nil];
521 [inputView delete:nil];
522 [inputView paste:nil];
523 [inputView paste:nil];
526 NSString* substring = [inputView textInRange:range];
527 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
530 - (void)testCanPerformActionForSelectActions {
531 NSDictionary* config =
self.mutableTemplateCopy;
532 [
self setClientId:123 configuration:config];
533 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
536 XCTAssertFalse([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
538 [inputView insertText:@"aaaa"];
540 XCTAssertTrue([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
543 - (void)testDeletingBackward {
544 NSDictionary* config =
self.mutableTemplateCopy;
545 [
self setClientId:123 configuration:config];
546 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
549 [inputView insertText:@"� ��� ��� text � ���� ���� ��� ���� ��� ���� ��� ���� ���� ���� ��� �� "];
550 [inputView deleteBackward];
551 [inputView deleteBackward];
554 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
555 [inputView deleteBackward];
556 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
557 [inputView deleteBackward];
558 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
559 [inputView deleteBackward];
560 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
561 [inputView deleteBackward];
563 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
564 [inputView deleteBackward];
565 [inputView deleteBackward];
566 [inputView deleteBackward];
567 [inputView deleteBackward];
568 [inputView deleteBackward];
569 [inputView deleteBackward];
571 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
572 [inputView deleteBackward];
573 XCTAssertEqualObjects(inputView.text,
@"ឹ");
574 [inputView deleteBackward];
575 XCTAssertEqualObjects(inputView.text,
@"");
580 - (void)testSystemOnlyAddingPartialComposedCharacter {
581 NSDictionary* config =
self.mutableTemplateCopy;
582 [
self setClientId:123 configuration:config];
583 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
586 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
587 [inputView deleteBackward];
590 [inputView insertText:[@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)]];
591 [inputView insertText:@"� ��"];
593 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
596 [inputView deleteBackward];
599 [inputView insertText:@"� ���"];
600 [inputView deleteBackward];
602 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
603 [inputView insertText:@"� ��"];
604 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
607 [inputView deleteBackward];
610 [inputView deleteBackward];
612 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
613 [inputView insertText:@"� ��"];
615 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
618 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
619 NSDictionary* config =
self.mutableTemplateCopy;
620 [
self setClientId:123 configuration:config];
621 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
624 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
625 [inputView deleteBackward];
626 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
629 NSString* brokenEmoji = [@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)];
630 [inputView insertText:brokenEmoji];
631 [inputView insertText:@"� ��"];
633 NSString* finalText = [NSString stringWithFormat:@"%@� ��", brokenEmoji];
634 XCTAssertEqualObjects(inputView.text, finalText);
637 - (void)testPastingNonTextDisallowed {
638 NSDictionary* config =
self.mutableTemplateCopy;
639 [
self setClientId:123 configuration:config];
640 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
643 UIPasteboard.generalPasteboard.color = UIColor.redColor;
644 XCTAssertNil(UIPasteboard.generalPasteboard.string);
645 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
646 [inputView paste:nil];
648 XCTAssertEqualObjects(inputView.text,
@"");
651 - (void)testNoZombies {
658 [passwordView.textField description];
660 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
663 - (void)testInputViewCrash {
668 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
669 activeView = inputPlugin.activeView;
671 [activeView updateEditingState];
674 - (void)testDoNotReuseInputViews {
675 NSDictionary* config =
self.mutableTemplateCopy;
676 [
self setClientId:123 configuration:config];
678 [
self setClientId:456 configuration:config];
680 XCTAssertNotNil(currentView);
685 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
687 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
691 - (void)testPropagatePressEventsToViewController {
693 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
694 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
698 NSDictionary* config =
self.mutableTemplateCopy;
699 [
self setClientId:123 configuration:config];
701 [
self setTextInputShow];
703 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
704 withEvent:OCMClassMock([UIPressesEvent class])];
706 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
707 withEvent:[OCMArg isNotNil]]);
708 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
709 withEvent:[OCMArg isNotNil]]);
711 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
712 withEvent:OCMClassMock([UIPressesEvent class])];
714 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
715 withEvent:[OCMArg isNotNil]]);
716 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
717 withEvent:[OCMArg isNotNil]]);
720 - (void)testPropagatePressEventsToViewController2 {
722 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
723 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
727 NSDictionary* config =
self.mutableTemplateCopy;
728 [
self setClientId:123 configuration:config];
729 [
self setTextInputShow];
732 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
733 withEvent:OCMClassMock([UIPressesEvent class])];
735 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
736 withEvent:[OCMArg isNotNil]]);
737 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
738 withEvent:[OCMArg isNotNil]]);
741 [
self setClientId:321 configuration:config];
742 [
self setTextInputShow];
744 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
746 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
747 withEvent:OCMClassMock([UIPressesEvent class])];
749 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
750 withEvent:[OCMArg isNotNil]]);
751 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
752 withEvent:[OCMArg isNotNil]]);
755 - (void)testUpdateSecureTextEntry {
756 NSDictionary* config =
self.mutableTemplateCopy;
757 [config setValue:@"YES" forKey:@"obscureText"];
758 [
self setClientId:123 configuration:config];
760 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
763 __block
int callCount = 0;
764 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
768 XCTAssertTrue(inputView.isSecureTextEntry);
770 config =
self.mutableTemplateCopy;
771 [config setValue:@"NO" forKey:@"obscureText"];
772 [
self updateConfig:config];
774 XCTAssertEqual(callCount, 1);
775 XCTAssertFalse(inputView.isSecureTextEntry);
778 - (void)testInputActionContinueAction {
794 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
796 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
799 - (void)testDisablingAutocorrectDisablesSpellChecking {
803 NSDictionary* config =
self.mutableTemplateCopy;
804 [inputView configureWithDictionary:config];
806 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
807 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
809 [config setValue:@(NO) forKey:@"autocorrect"];
810 [inputView configureWithDictionary:config];
812 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
813 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
816 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
818 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
832 XCTAssertEqual(inputView.markedTextRange, nil);
835 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
837 BOOL respondsToInsertionPointColor =
838 [inputView respondsToSelector:@selector(insertionPointColor)];
839 if (@available(iOS 17, *)) {
840 XCTAssertFalse(respondsToInsertionPointColor);
842 XCTAssertTrue(respondsToInsertionPointColor);
846 #pragma mark - TextEditingDelta tests
847 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
849 inputView.enableDeltaModel = YES;
851 __block
int updateCount = 0;
853 [inputView insertText:@"text to insert"];
856 flutterTextInputView:inputView
857 updateEditingClient:0
858 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
859 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
860 isEqualToString:
@""]) &&
861 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
862 isEqualToString:
@"text to insert"]) &&
863 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
864 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
866 .andDo(^(NSInvocation* invocation) {
869 XCTAssertEqual(updateCount, 0);
871 [
self flushScheduledAsyncBlocks];
874 XCTAssertEqual(updateCount, 1);
876 [inputView deleteBackward];
877 OCMExpect([
engine flutterTextInputView:inputView
878 updateEditingClient:0
879 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
880 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
881 isEqualToString:
@"text to insert"]) &&
882 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
883 isEqualToString:
@""]) &&
884 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
886 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
889 .andDo(^(NSInvocation* invocation) {
892 [
self flushScheduledAsyncBlocks];
893 XCTAssertEqual(updateCount, 2);
896 OCMExpect([
engine flutterTextInputView:inputView
897 updateEditingClient:0
898 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
899 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
900 isEqualToString:
@"text to inser"]) &&
901 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
902 isEqualToString:
@""]) &&
903 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
905 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
908 .andDo(^(NSInvocation* invocation) {
911 [
self flushScheduledAsyncBlocks];
912 XCTAssertEqual(updateCount, 3);
915 withText:@"replace text"];
918 flutterTextInputView:inputView
919 updateEditingClient:0
920 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
921 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
922 isEqualToString:
@"text to inser"]) &&
923 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
924 isEqualToString:
@"replace text"]) &&
925 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
926 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
928 .andDo(^(NSInvocation* invocation) {
931 [
self flushScheduledAsyncBlocks];
932 XCTAssertEqual(updateCount, 4);
934 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
935 OCMExpect([
engine flutterTextInputView:inputView
936 updateEditingClient:0
937 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
938 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
939 isEqualToString:
@"replace textext to inser"]) &&
940 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
941 isEqualToString:
@"marked text"]) &&
942 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
944 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
947 .andDo(^(NSInvocation* invocation) {
950 [
self flushScheduledAsyncBlocks];
951 XCTAssertEqual(updateCount, 5);
953 [inputView unmarkText];
955 flutterTextInputView:inputView
956 updateEditingClient:0
957 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
958 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
959 isEqualToString:
@"replace textmarked textext to inser"]) &&
960 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
961 isEqualToString:
@""]) &&
962 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
964 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
967 .andDo(^(NSInvocation* invocation) {
970 [
self flushScheduledAsyncBlocks];
972 XCTAssertEqual(updateCount, 6);
976 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
979 inputView.enableDeltaModel = YES;
982 OCMExpect([
engine flutterTextInputView:inputView
983 updateEditingClient:0
984 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
985 NSArray* deltas = state[@"deltas"];
986 NSDictionary* firstDelta = deltas[0];
987 NSDictionary* secondDelta = deltas[1];
988 NSDictionary* thirdDelta = deltas[2];
989 return [firstDelta[@"oldText"] isEqualToString:@""] &&
990 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
991 [firstDelta[@"deltaStart"] intValue] == 0 &&
992 [firstDelta[@"deltaEnd"] intValue] == 0 &&
993 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
994 [secondDelta[@"deltaText"] isEqualToString:@""] &&
995 [secondDelta[@"deltaStart"] intValue] == 0 &&
996 [secondDelta[@"deltaEnd"] intValue] == 1 &&
997 [thirdDelta[@"oldText"] isEqualToString:@""] &&
998 [thirdDelta[@"deltaText"] isEqualToString:@"� ��"] &&
999 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1000 [thirdDelta[@"deltaEnd"] intValue] == 0;
1004 [inputView insertText:@"-"];
1005 [inputView deleteBackward];
1006 [inputView insertText:@"� ��"];
1008 [
self flushScheduledAsyncBlocks];
1012 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1014 inputView.enableDeltaModel = YES;
1016 __block
int updateCount = 0;
1017 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1018 .andDo(^(NSInvocation* invocation) {
1022 [inputView.text setString:@"Some initial text"];
1023 XCTAssertEqual(updateCount, 0);
1026 inputView.markedTextRange = range;
1027 inputView.selectedTextRange = nil;
1028 [
self flushScheduledAsyncBlocks];
1029 XCTAssertEqual(updateCount, 1);
1031 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1033 flutterTextInputView:inputView
1034 updateEditingClient:0
1035 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1036 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1037 isEqualToString:
@"Some initial text"]) &&
1038 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1039 isEqualToString:
@"new marked text."]) &&
1040 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1041 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1043 [
self flushScheduledAsyncBlocks];
1044 XCTAssertEqual(updateCount, 2);
1047 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1049 inputView.enableDeltaModel = YES;
1051 __block
int updateCount = 0;
1052 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1053 .andDo(^(NSInvocation* invocation) {
1057 [inputView.text setString:@"Some initial text"];
1058 [
self flushScheduledAsyncBlocks];
1059 XCTAssertEqual(updateCount, 0);
1062 inputView.markedTextRange = range;
1063 inputView.selectedTextRange = nil;
1064 [
self flushScheduledAsyncBlocks];
1065 XCTAssertEqual(updateCount, 1);
1067 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1069 flutterTextInputView:inputView
1070 updateEditingClient:0
1071 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1072 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1073 isEqualToString:
@"Some initial text"]) &&
1074 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1075 isEqualToString:
@"text."]) &&
1076 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1077 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1079 [
self flushScheduledAsyncBlocks];
1080 XCTAssertEqual(updateCount, 2);
1083 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1085 inputView.enableDeltaModel = YES;
1087 __block
int updateCount = 0;
1088 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1089 .andDo(^(NSInvocation* invocation) {
1093 [inputView.text setString:@"Some initial text"];
1094 [
self flushScheduledAsyncBlocks];
1095 XCTAssertEqual(updateCount, 0);
1098 inputView.markedTextRange = range;
1099 inputView.selectedTextRange = nil;
1100 [
self flushScheduledAsyncBlocks];
1101 XCTAssertEqual(updateCount, 1);
1103 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1105 flutterTextInputView:inputView
1106 updateEditingClient:0
1107 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1108 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1109 isEqualToString:
@"Some initial text"]) &&
1110 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1111 isEqualToString:
@"tex"]) &&
1112 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1113 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1115 [
self flushScheduledAsyncBlocks];
1116 XCTAssertEqual(updateCount, 2);
1119 #pragma mark - EditingState tests
1121 - (void)testUITextInputCallsUpdateEditingStateOnce {
1124 __block
int updateCount = 0;
1125 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1126 .andDo(^(NSInvocation* invocation) {
1130 [inputView insertText:@"text to insert"];
1132 XCTAssertEqual(updateCount, 1);
1134 [inputView deleteBackward];
1135 XCTAssertEqual(updateCount, 2);
1138 XCTAssertEqual(updateCount, 3);
1141 withText:@"replace text"];
1142 XCTAssertEqual(updateCount, 4);
1144 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1145 XCTAssertEqual(updateCount, 5);
1147 [inputView unmarkText];
1148 XCTAssertEqual(updateCount, 6);
1151 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1153 inputView.enableDeltaModel = YES;
1155 __block
int updateCount = 0;
1156 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1157 .andDo(^(NSInvocation* invocation) {
1161 [inputView insertText:@"text to insert"];
1162 [
self flushScheduledAsyncBlocks];
1164 XCTAssertEqual(updateCount, 1);
1166 [inputView deleteBackward];
1167 [
self flushScheduledAsyncBlocks];
1168 XCTAssertEqual(updateCount, 2);
1171 [
self flushScheduledAsyncBlocks];
1172 XCTAssertEqual(updateCount, 3);
1175 withText:@"replace text"];
1176 [
self flushScheduledAsyncBlocks];
1177 XCTAssertEqual(updateCount, 4);
1179 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1180 [
self flushScheduledAsyncBlocks];
1181 XCTAssertEqual(updateCount, 5);
1183 [inputView unmarkText];
1184 [
self flushScheduledAsyncBlocks];
1185 XCTAssertEqual(updateCount, 6);
1188 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1191 __block
int updateCount = 0;
1192 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1193 .andDo(^(NSInvocation* invocation) {
1197 [inputView.text setString:@"BEFORE"];
1198 XCTAssertEqual(updateCount, 0);
1200 inputView.markedTextRange = nil;
1201 inputView.selectedTextRange = nil;
1202 XCTAssertEqual(updateCount, 1);
1205 XCTAssertEqual(updateCount, 1);
1206 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1207 XCTAssertEqual(updateCount, 1);
1208 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1209 XCTAssertEqual(updateCount, 1);
1213 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1214 XCTAssertEqual(updateCount, 1);
1216 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1217 XCTAssertEqual(updateCount, 1);
1221 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1222 XCTAssertEqual(updateCount, 1);
1224 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1225 XCTAssertEqual(updateCount, 1);
1228 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1230 inputView.enableDeltaModel = YES;
1232 __block
int updateCount = 0;
1233 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1234 .andDo(^(NSInvocation* invocation) {
1238 [inputView.text setString:@"BEFORE"];
1239 [
self flushScheduledAsyncBlocks];
1240 XCTAssertEqual(updateCount, 0);
1242 inputView.markedTextRange = nil;
1243 inputView.selectedTextRange = nil;
1244 [
self flushScheduledAsyncBlocks];
1245 XCTAssertEqual(updateCount, 1);
1248 XCTAssertEqual(updateCount, 1);
1249 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1250 [
self flushScheduledAsyncBlocks];
1251 XCTAssertEqual(updateCount, 1);
1253 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1254 [
self flushScheduledAsyncBlocks];
1255 XCTAssertEqual(updateCount, 1);
1259 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1260 [
self flushScheduledAsyncBlocks];
1261 XCTAssertEqual(updateCount, 1);
1264 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1265 [
self flushScheduledAsyncBlocks];
1266 XCTAssertEqual(updateCount, 1);
1270 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1271 [
self flushScheduledAsyncBlocks];
1272 XCTAssertEqual(updateCount, 1);
1275 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1276 [
self flushScheduledAsyncBlocks];
1277 XCTAssertEqual(updateCount, 1);
1280 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1283 __block
int updateCount = 0;
1284 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1285 .andDo(^(NSInvocation* invocation) {
1289 [inputView unmarkText];
1291 XCTAssertEqual(updateCount, 0);
1293 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1295 XCTAssertEqual(updateCount, 1);
1297 [inputView unmarkText];
1299 XCTAssertEqual(updateCount, 2);
1302 - (void)testCanCopyPasteWithScribbleEnabled {
1303 if (@available(iOS 14.0, *)) {
1304 NSDictionary* config =
self.mutableTemplateCopy;
1305 [
self setClientId:123 configuration:config];
1306 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1312 [mockInputView insertText:@"aaaa"];
1313 [mockInputView selectAll:nil];
1315 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1316 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1317 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1318 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1320 [mockInputView copy:NULL];
1321 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1322 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1323 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1324 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1328 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1329 if (@available(iOS 14.0, *)) {
1332 __block
int updateCount = 0;
1333 OCMStub([
engine flutterTextInputView:inputView
1334 updateEditingClient:0
1335 withState:[OCMArg isNotNil]])
1336 .andDo(^(NSInvocation* invocation) {
1340 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1342 XCTAssertEqual(updateCount, 1);
1344 UIScribbleInteraction* scribbleInteraction =
1345 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1347 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1348 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1350 XCTAssertEqual(updateCount, 1);
1352 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1353 [inputView resetScribbleInteractionStatusIfEnding];
1354 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1356 XCTAssertEqual(updateCount, 2);
1358 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1359 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1362 XCTAssertEqual(updateCount, 2);
1364 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1365 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1368 XCTAssertEqual(updateCount, 2);
1370 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1371 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1373 XCTAssertEqual(updateCount, 3);
1377 - (void)testUpdateEditingClientNegativeSelection {
1380 [inputView.text setString:@"SELECTION"];
1381 inputView.markedTextRange = nil;
1382 inputView.selectedTextRange = nil;
1384 [inputView setTextInputState:@{
1385 @"text" : @"SELECTION",
1386 @"selectionBase" : @-1,
1387 @"selectionExtent" : @-1
1389 [inputView updateEditingState];
1390 OCMVerify([
engine flutterTextInputView:inputView
1391 updateEditingClient:0
1392 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1393 return ([state[
@"selectionBase"] intValue]) == 0 &&
1394 ([state[
@"selectionExtent"] intValue] == 0);
1399 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1400 [inputView updateEditingState];
1401 OCMVerify([
engine flutterTextInputView:inputView
1402 updateEditingClient:0
1403 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1404 return ([state[
@"selectionBase"] intValue]) == 0 &&
1405 ([state[
@"selectionExtent"] intValue] == 0);
1409 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1410 [inputView updateEditingState];
1411 OCMVerify([
engine flutterTextInputView:inputView
1412 updateEditingClient:0
1413 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1414 return ([state[
@"selectionBase"] intValue]) == 0 &&
1415 ([state[
@"selectionExtent"] intValue] == 0);
1419 - (void)testUpdateEditingClientSelectionClamping {
1423 [inputView.text setString:@"SELECTION"];
1424 inputView.markedTextRange = nil;
1425 inputView.selectedTextRange = nil;
1428 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1429 [inputView updateEditingState];
1430 OCMVerify([
engine flutterTextInputView:inputView
1431 updateEditingClient:0
1432 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1433 return ([state[
@"selectionBase"] intValue]) == 0 &&
1434 ([state[
@"selectionExtent"] intValue] == 0);
1438 [inputView setTextInputState:@{
1439 @"text" : @"SELECTION",
1440 @"selectionBase" : @0,
1441 @"selectionExtent" : @9999
1443 [inputView updateEditingState];
1445 OCMVerify([
engine flutterTextInputView:inputView
1446 updateEditingClient:0
1447 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1448 return ([state[
@"selectionBase"] intValue]) == 0 &&
1449 ([state[
@"selectionExtent"] intValue] == 9);
1454 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1455 [inputView updateEditingState];
1456 OCMVerify([
engine flutterTextInputView:inputView
1457 updateEditingClient:0
1458 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1459 return ([state[
@"selectionBase"] intValue]) == 0 &&
1460 ([state[
@"selectionExtent"] intValue] == 1);
1464 [inputView setTextInputState:@{
1465 @"text" : @"SELECTION",
1466 @"selectionBase" : @9999,
1467 @"selectionExtent" : @9999
1469 [inputView updateEditingState];
1470 OCMVerify([
engine flutterTextInputView:inputView
1471 updateEditingClient:0
1472 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1473 return ([state[
@"selectionBase"] intValue]) == 9 &&
1474 ([state[
@"selectionExtent"] intValue] == 9);
1478 - (void)testInputViewsHasNonNilInputDelegate {
1479 if (@available(iOS 13.0, *)) {
1481 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1483 [inputView setTextInputClient:123];
1484 [inputView reloadInputViews];
1485 [inputView becomeFirstResponder];
1486 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1487 inputView.inputDelegate = nil;
1490 [mockInputView setTextInputState:@{
1491 @"text" : @"COMPOSING",
1492 @"composingBase" : @1,
1493 @"composingExtent" : @3
1495 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1496 [inputView removeFromSuperview];
1500 - (void)testInputViewsDoNotHaveUITextInteractions {
1501 if (@available(iOS 13.0, *)) {
1503 BOOL hasTextInteraction = NO;
1504 for (
id interaction in inputView.interactions) {
1505 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1506 if (hasTextInteraction) {
1510 XCTAssertFalse(hasTextInteraction);
1514 #pragma mark - UITextInput methods - Tests
1516 - (void)testUpdateFirstRectForRange {
1517 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1523 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1528 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1529 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1533 NSArray* affineMatrix = @[
1534 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1535 @(-6.0), @(3.0), @(9.0), @(1.0)
1539 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1541 [inputView setEditableTransform:yOffsetMatrix];
1543 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1546 CGRect testRect = CGRectMake(0, 0, 100, 100);
1547 [inputView setMarkedRect:testRect];
1549 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1550 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1552 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1555 [inputView setEditableTransform:zeroMatrix];
1557 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1558 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1561 [inputView setEditableTransform:yOffsetMatrix];
1562 [inputView setMarkedRect:testRect];
1563 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1566 [inputView setMarkedRect:kInvalidFirstRect];
1568 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1569 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1572 [inputView setEditableTransform:affineMatrix];
1573 [inputView setMarkedRect:testRect];
1575 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1577 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1578 const CGPoint offset = CGPointMake(113, 119);
1579 CGRect currentFrame = inputView.frame;
1580 currentFrame.origin = offset;
1581 inputView.frame = currentFrame;
1584 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1585 [inputView firstRectForRange:range]));
1588 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1590 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1595 [inputView setSelectionRects:@[
1604 if (@available(iOS 17, *)) {
1605 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1606 [inputView firstRectForRange:multiRectRange]));
1608 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1609 [inputView firstRectForRange:multiRectRange]));
1613 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1615 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1617 [inputView setSelectionRects:@[
1624 if (@available(iOS 17, *)) {
1625 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1626 [inputView firstRectForRange:singleRectRange]));
1628 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1633 if (@available(iOS 17, *)) {
1634 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1635 [inputView firstRectForRange:multiRectRange]));
1637 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1640 [inputView setTextInputState:@{@"text" : @"COM"}];
1642 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1645 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1647 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1649 [inputView setSelectionRects:@[
1656 if (@available(iOS 17, *)) {
1657 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1658 [inputView firstRectForRange:singleRectRange]));
1660 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1664 if (@available(iOS 17, *)) {
1665 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1666 [inputView firstRectForRange:multiRectRange]));
1668 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1671 [inputView setTextInputState:@{@"text" : @"COM"}];
1673 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1676 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1678 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1680 [inputView setSelectionRects:@[
1691 if (@available(iOS 17, *)) {
1692 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1693 [inputView firstRectForRange:singleRectRange]));
1695 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1700 if (@available(iOS 17, *)) {
1701 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1702 [inputView firstRectForRange:multiRectRange]));
1704 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1708 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1710 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1712 [inputView setSelectionRects:@[
1723 if (@available(iOS 17, *)) {
1724 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1725 [inputView firstRectForRange:singleRectRange]));
1727 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1731 if (@available(iOS 17, *)) {
1732 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1733 [inputView firstRectForRange:multiRectRange]));
1735 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1739 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1741 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1743 [inputView setSelectionRects:@[
1754 if (@available(iOS 17, *)) {
1755 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1756 [inputView firstRectForRange:multiRectRange]));
1758 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1762 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1764 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1766 [inputView setSelectionRects:@[
1777 if (@available(iOS 17, *)) {
1778 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1779 [inputView firstRectForRange:multiRectRange]));
1781 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1785 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1787 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1789 [inputView setSelectionRects:@[
1800 if (@available(iOS 17, *)) {
1801 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1802 [inputView firstRectForRange:multiRectRange]));
1804 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1808 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1810 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1812 [inputView setSelectionRects:@[
1823 if (@available(iOS 17, *)) {
1824 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1825 [inputView firstRectForRange:multiRectRange]));
1827 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1831 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1833 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1835 [inputView setSelectionRects:@[
1846 if (@available(iOS 17, *)) {
1847 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1848 [inputView firstRectForRange:multiRectRange]));
1850 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1854 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1856 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1858 [inputView setSelectionRects:@[
1869 if (@available(iOS 17, *)) {
1870 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1871 [inputView firstRectForRange:multiRectRange]));
1873 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1877 - (void)testClosestPositionToPoint {
1879 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1882 [inputView setSelectionRects:@[
1887 CGPoint point = CGPointMake(150, 150);
1888 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1889 XCTAssertEqual(UITextStorageDirectionBackward,
1894 [inputView setSelectionRects:@[
1901 point = CGPointMake(125, 150);
1902 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1903 XCTAssertEqual(UITextStorageDirectionForward,
1908 [inputView setSelectionRects:@[
1915 point = CGPointMake(125, 201);
1916 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1917 XCTAssertEqual(UITextStorageDirectionBackward,
1921 [inputView setSelectionRects:@[
1927 point = CGPointMake(125, 250);
1928 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1929 XCTAssertEqual(UITextStorageDirectionBackward,
1933 [inputView setSelectionRects:@[
1938 point = CGPointMake(110, 50);
1939 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1940 XCTAssertEqual(UITextStorageDirectionForward,
1945 [inputView beginFloatingCursorAtPoint:CGPointZero];
1946 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1947 XCTAssertEqual(UITextStorageDirectionForward,
1949 [inputView endFloatingCursor];
1952 - (void)testClosestPositionToPointRTL {
1954 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1956 [inputView setSelectionRects:@[
1972 XCTAssertEqual(0U, position.
index);
1973 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1975 XCTAssertEqual(1U, position.
index);
1976 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1978 XCTAssertEqual(1U, position.
index);
1979 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1981 XCTAssertEqual(2U, position.
index);
1982 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1984 XCTAssertEqual(2U, position.
index);
1985 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1987 XCTAssertEqual(3U, position.
index);
1988 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1990 XCTAssertEqual(3U, position.
index);
1991 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1994 - (void)testSelectionRectsForRange {
1996 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1998 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
1999 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2000 [inputView setSelectionRects:@[
2009 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2010 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2011 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2015 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2016 XCTAssertTrue(CGRectEqualToRect(
2017 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2018 [inputView selectionRectsForRange:range][0].rect));
2021 - (void)testClosestPositionToPointWithinRange {
2023 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2026 [inputView setSelectionRects:@[
2033 CGPoint point = CGPointMake(125, 150);
2036 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2038 UITextStorageDirectionForward,
2039 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2042 [inputView setSelectionRects:@[
2049 point = CGPointMake(125, 150);
2052 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2054 UITextStorageDirectionForward,
2055 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2058 - (void)testClosestPositionToPointWithPartialSelectionRects {
2060 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2067 XCTAssertTrue(CGRectEqualToRect(
2070 affinity:UITextStorageDirectionForward]],
2071 CGRectMake(100, 0, 0, 100)));
2074 XCTAssertTrue(CGRectEqualToRect(
2077 affinity:UITextStorageDirectionForward]],
2081 #pragma mark - Floating Cursor - Tests
2083 - (void)testFloatingCursorDoesNotThrow {
2086 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2087 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2088 [inputView endFloatingCursor];
2089 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2090 [inputView endFloatingCursor];
2093 - (void)testFloatingCursor {
2095 [inputView setTextInputState:@{
2097 @"selectionBase" : @1,
2098 @"selectionExtent" : @1,
2109 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2112 XCTAssertTrue(CGRectEqualToRect(
2115 affinity:UITextStorageDirectionForward]],
2116 CGRectMake(0, 0, 0, 100)));
2119 XCTAssertTrue(CGRectEqualToRect(
2122 affinity:UITextStorageDirectionForward]],
2123 CGRectMake(100, 100, 0, 100)));
2124 XCTAssertTrue(CGRectEqualToRect(
2127 affinity:UITextStorageDirectionForward]],
2128 CGRectMake(200, 200, 0, 100)));
2129 XCTAssertTrue(CGRectEqualToRect(
2132 affinity:UITextStorageDirectionForward]],
2133 CGRectMake(300, 300, 0, 100)));
2136 XCTAssertTrue(CGRectEqualToRect(
2139 affinity:UITextStorageDirectionForward]],
2140 CGRectMake(400, 300, 0, 100)));
2142 XCTAssertTrue(CGRectEqualToRect(
2145 affinity:UITextStorageDirectionForward]],
2149 [inputView setTextInputState:@{
2151 @"selectionBase" : @2,
2152 @"selectionExtent" : @2,
2155 XCTAssertTrue(CGRectEqualToRect(
2158 affinity:UITextStorageDirectionBackward]],
2159 CGRectMake(0, 0, 0, 100)));
2162 XCTAssertTrue(CGRectEqualToRect(
2165 affinity:UITextStorageDirectionBackward]],
2166 CGRectMake(100, 0, 0, 100)));
2167 XCTAssertTrue(CGRectEqualToRect(
2170 affinity:UITextStorageDirectionBackward]],
2171 CGRectMake(200, 100, 0, 100)));
2172 XCTAssertTrue(CGRectEqualToRect(
2175 affinity:UITextStorageDirectionBackward]],
2176 CGRectMake(300, 200, 0, 100)));
2177 XCTAssertTrue(CGRectEqualToRect(
2180 affinity:UITextStorageDirectionBackward]],
2181 CGRectMake(400, 300, 0, 100)));
2183 XCTAssertTrue(CGRectEqualToRect(
2186 affinity:UITextStorageDirectionBackward]],
2191 CGRect initialBounds = inputView.bounds;
2192 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2193 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2194 OCMVerify([
engine flutterTextInputView:inputView
2195 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2197 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2198 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2199 ([state[
@"Y"] isEqualToNumber:@(0)]);
2202 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2203 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2204 OCMVerify([
engine flutterTextInputView:inputView
2205 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2207 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2208 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2209 ([state[
@"Y"] isEqualToNumber:@(333)]);
2212 [inputView endFloatingCursor];
2213 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2214 OCMVerify([
engine flutterTextInputView:inputView
2215 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2217 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2218 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2219 ([state[
@"Y"] isEqualToNumber:@(0)]);
2223 #pragma mark - UIKeyInput Overrides - Tests
2225 - (void)testInsertTextAddsPlaceholderSelectionRects {
2228 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2238 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2241 [inputView insertText:@"in"];
2269 #pragma mark - Autofill - Utilities
2271 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2274 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2275 @"keyboardAppearance" :
@"Brightness.light",
2276 @"obscureText" : @YES,
2277 @"inputAction" :
@"TextInputAction.unspecified",
2278 @"smartDashesType" :
@"0",
2279 @"smartQuotesType" :
@"0",
2280 @"autocorrect" : @YES
2284 return [_passwordTemplate mutableCopy];
2288 return [
self.installedInputViews
2289 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2292 - (void)commitAutofillContextAndVerify {
2296 [textInputPlugin handleMethodCall:methodCall
2297 result:^(id _Nullable result){
2300 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2305 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2309 #pragma mark - Autofill - Tests
2311 - (void)testDisablingAutofillOnInputClient {
2312 NSDictionary* config =
self.mutableTemplateCopy;
2313 [config setValue:@"YES" forKey:@"obscureText"];
2315 [
self setClientId:123 configuration:config];
2318 XCTAssertEqualObjects(inputView.textContentType,
@"");
2321 - (void)testAutofillEnabledByDefault {
2322 NSDictionary* config =
self.mutableTemplateCopy;
2323 [config setValue:@"NO" forKey:@"obscureText"];
2324 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2325 forKey:@"autofill"];
2327 [
self setClientId:123 configuration:config];
2330 XCTAssertNil(inputView.textContentType);
2333 - (void)testAutofillContext {
2334 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2337 @"uniqueIdentifier" : @"field1",
2338 @"hints" : @[ @"hint1" ],
2339 @"editingValue" : @{@"text" : @""}
2341 forKey:@"autofill"];
2343 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2345 @"uniqueIdentifier" : @"field2",
2346 @"hints" : @[ @"hint2" ],
2347 @"editingValue" : @{@"text" : @""}
2349 forKey:@"autofill"];
2351 NSMutableDictionary* config = [field1 mutableCopy];
2352 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2354 [
self setClientId:123 configuration:config];
2355 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2359 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2360 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2362 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2365 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2367 @"uniqueIdentifier" : @"field3",
2368 @"hints" : @[ @"hint3" ],
2369 @"editingValue" : @{@"text" : @""}
2371 forKey:@"autofill"];
2375 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2377 [
self setClientId:123 configuration:config];
2379 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2382 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2383 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2385 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2388 for (NSString* key in oldContext.allKeys) {
2389 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2393 config =
self.mutablePasswordTemplateCopy;
2396 [
self setClientId:124 configuration:config];
2397 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2399 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2402 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2403 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2406 for (NSString* key in oldContext.allKeys) {
2407 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2411 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2415 [
self setClientId:200 configuration:config];
2418 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2421 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2422 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2425 for (NSString* key in oldContext.allKeys) {
2426 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2429 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2432 - (void)testCommitAutofillContext {
2433 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2435 @"uniqueIdentifier" : @"field1",
2436 @"hints" : @[ @"hint1" ],
2437 @"editingValue" : @{@"text" : @""}
2439 forKey:@"autofill"];
2441 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2443 @"uniqueIdentifier" : @"field2",
2444 @"hints" : @[ @"hint2" ],
2445 @"editingValue" : @{@"text" : @""}
2447 forKey:@"autofill"];
2449 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2451 @"uniqueIdentifier" : @"field3",
2452 @"hints" : @[ @"hint3" ],
2453 @"editingValue" : @{@"text" : @""}
2455 forKey:@"autofill"];
2457 NSMutableDictionary* config = [field1 mutableCopy];
2458 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2460 [
self setClientId:123 configuration:config];
2461 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2463 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2465 [
self commitAutofillContextAndVerify];
2466 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2469 [
self setClientId:123 configuration:config];
2471 [
self setClientId:124 configuration:field3];
2472 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2474 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2475 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2478 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2480 [
self commitAutofillContextAndVerify];
2481 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2484 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2486 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2490 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2491 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2493 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2495 [
self commitAutofillContextAndVerify];
2496 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2499 - (void)testAutofillInputViews {
2500 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2502 @"uniqueIdentifier" : @"field1",
2503 @"hints" : @[ @"hint1" ],
2504 @"editingValue" : @{@"text" : @""}
2506 forKey:@"autofill"];
2508 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2510 @"uniqueIdentifier" : @"field2",
2511 @"hints" : @[ @"hint2" ],
2512 @"editingValue" : @{@"text" : @""}
2514 forKey:@"autofill"];
2516 NSMutableDictionary* config = [field1 mutableCopy];
2517 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2519 [
self setClientId:123 configuration:config];
2520 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2523 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2526 XCTAssertEqual(inputFields.count, 2ul);
2527 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2532 withText:@"Autofilled!"];
2533 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2536 OCMVerify([
engine flutterTextInputView:inactiveView
2537 updateEditingClient:0
2538 withState:[OCMArg isNotNil]
2539 withTag:
@"field2"]);
2542 - (void)testPasswordAutofillHack {
2543 NSDictionary* config =
self.mutableTemplateCopy;
2544 [config setValue:@"YES" forKey:@"obscureText"];
2545 [
self setClientId:123 configuration:config];
2548 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2552 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2555 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2558 - (void)testClearAutofillContextClearsSelection {
2559 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2560 NSDictionary* editingValue = @{
2561 @"text" :
@"REGULAR_TEXT_FIELD",
2562 @"composingBase" : @0,
2563 @"composingExtent" : @3,
2564 @"selectionBase" : @1,
2565 @"selectionExtent" : @4
2567 [regularField setValue:@{
2568 @"uniqueIdentifier" : @"field2",
2569 @"hints" : @[ @"hint2" ],
2570 @"editingValue" : editingValue,
2572 forKey:@"autofill"];
2573 [regularField addEntriesFromDictionary:editingValue];
2574 [
self setClientId:123 configuration:regularField];
2575 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2576 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2579 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2581 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2585 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2586 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2588 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2590 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2591 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2594 XCTAssert([oldInputView.text isEqualToString:
@""]);
2596 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2599 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2601 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2602 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2604 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2607 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2608 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2610 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2612 [
self commitAutofillContextAndVerify];
2615 - (void)testScribbleSetSelectionRects {
2616 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2617 NSDictionary* editingValue = @{
2618 @"text" :
@"REGULAR_TEXT_FIELD",
2619 @"composingBase" : @0,
2620 @"composingExtent" : @3,
2621 @"selectionBase" : @1,
2622 @"selectionExtent" : @4
2624 [regularField setValue:@{
2625 @"uniqueIdentifier" : @"field1",
2626 @"hints" : @[ @"hint2" ],
2627 @"editingValue" : editingValue,
2629 forKey:@"autofill"];
2630 [regularField addEntriesFromDictionary:editingValue];
2631 [
self setClientId:123 configuration:regularField];
2632 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2633 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2635 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2636 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2640 [textInputPlugin handleMethodCall:methodCall
2641 result:^(id _Nullable result){
2644 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2647 - (void)testDecommissionedViewAreNotReusedByAutofill {
2649 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2650 [configuration setValue:@{
2651 @"uniqueIdentifier" : @"field1",
2652 @"hints" : @[ UITextContentTypePassword ],
2653 @"editingValue" : @{@"text" : @""}
2655 forKey:@"autofill"];
2656 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2658 [
self setClientId:123 configuration:configuration];
2660 [
self setTextInputHide];
2663 [
self setClientId:124 configuration:configuration];
2667 XCTAssertNotNil(previousActiveView);
2671 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2678 #pragma mark - Accessibility - Tests
2680 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2681 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2684 [
self setTextInputShow];
2686 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2689 XCTAssertEqual([inputFields count], 1u);
2692 [
self setTextInputHide];
2694 inputFields =
self.installedInputViews;
2697 XCTAssertEqual([inputFields count], 0u);
2700 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2703 UIView* container = [[UIView alloc] init];
2704 UIAccessibilityElement* backing =
2705 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2706 inputView.backingTextInputAccessibilityObject = backing;
2709 [inputView accessibilityElementDidBecomeFocused];
2715 - (void)testFlutterTokenizerCanParseLines {
2717 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2720 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2721 XCTAssertEqual(range.
range.location, 0u);
2722 XCTAssertEqual(range.
range.length, 0u);
2724 [inputView insertText:@"how are you\nI am fine, Thank you"];
2726 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2727 XCTAssertEqual(range.
range.location, 0u);
2728 XCTAssertEqual(range.
range.length, 11u);
2730 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2731 XCTAssertEqual(range.
range.location, 0u);
2732 XCTAssertEqual(range.
range.length, 11u);
2734 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2735 XCTAssertEqual(range.
range.location, 0u);
2736 XCTAssertEqual(range.
range.length, 11u);
2738 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2739 XCTAssertEqual(range.
range.location, 12u);
2740 XCTAssertEqual(range.
range.length, 20u);
2742 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2743 XCTAssertEqual(range.
range.location, 12u);
2744 XCTAssertEqual(range.
range.length, 20u);
2746 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2747 XCTAssertEqual(range.
range.location, 12u);
2748 XCTAssertEqual(range.
range.length, 20u);
2751 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2753 [inputView insertText:@"0123456789\n012345"];
2754 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2757 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2758 withGranularity:UITextGranularityLine
2759 inDirection:UITextStorageDirectionBackward];
2760 XCTAssertEqual(range.
range.location, 11u);
2761 XCTAssertEqual(range.
range.length, 6u);
2764 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2766 [inputView insertText:@"0123456789\n012345"];
2767 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2770 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2771 withGranularity:UITextGranularityLine
2772 inDirection:UITextStorageDirectionForward];
2773 if (@available(iOS 17.0, *)) {
2774 XCTAssertNil(range);
2776 XCTAssertEqual(range.
range.location, 11u);
2777 XCTAssertEqual(range.
range.length, 6u);
2781 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2783 [inputView insertText:@"0123456789\n012345"];
2784 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2789 withGranularity:UITextGranularityLine
2790 inDirection:UITextStorageDirectionForward];
2791 if (@available(iOS 17.0, *)) {
2792 XCTAssertNil(range);
2794 XCTAssertEqual(range.
range.location, 0u);
2795 XCTAssertEqual(range.
range.length, 0u);
2799 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2804 __weak UIView* activeView;
2809 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2812 result:^(id _Nullable result){
2818 result:^(id _Nullable result){
2820 XCTAssertNotNil(activeView);
2823 XCTAssertNotNil(activeView);
2826 - (void)testFlutterTextInputPluginHostViewNilCrash {
2829 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2832 - (void)testFlutterTextInputPluginHostViewNotNil {
2838 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2841 - (void)testSetPlatformViewClient {
2848 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2850 result:^(id _Nullable result){
2853 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2856 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2858 result:^(id _Nullable result){
2860 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2863 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
2864 if (@available(iOS 16.0, *)) {
2866 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2867 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
2868 @"editMenuInteraction setup delegate correctly");
2872 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
2873 if (@available(iOS 16.0, *)) {
2877 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
2881 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
2882 if (@available(iOS 16.0, *)) {
2887 [myViewController loadView];
2890 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2892 result:^(id _Nullable result){
2898 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2900 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2901 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2903 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2904 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2905 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2906 .andDo(^(NSInvocation* invocation) {
2908 [invocation retainArguments];
2909 UIEditMenuConfiguration* config;
2910 [invocation getArgument:&config atIndex:2];
2911 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
2912 @"UIEditMenuConfiguration must use automatic arrow direction.");
2913 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
2914 @"UIEditMenuConfiguration must have the correct point.");
2915 [expectation fulfill];
2918 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2919 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
2921 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2922 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2923 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2927 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
2928 if (@available(iOS 16.0, *)) {
2933 [myViewController loadView];
2937 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2939 result:^(id _Nullable result){
2945 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2947 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2948 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2950 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2951 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2952 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2953 .andDo(^(NSInvocation* invocation) {
2954 [expectation fulfill];
2957 myInputView.frame = CGRectMake(10, 20, 30, 40);
2958 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2959 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
2961 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2962 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2963 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2966 [myInputView editMenuInteraction:mockInteraction
2967 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
2969 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
2970 @"targetRectForConfiguration must return the correct target rect.");
2974 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
2976 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2978 [inputView setTextInputClient:123];
2979 [inputView reloadInputViews];
2980 [inputView becomeFirstResponder];
2981 XCTAssert(inputView.isFirstResponder);
2983 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2984 [NSNotificationCenter.defaultCenter
2985 postNotificationName:UIKeyboardWillShowNotification
2987 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2991 [textInputPlugin handleMethodCall:onPointerMoveCall
2992 result:^(id _Nullable result){
2994 XCTAssertFalse(inputView.isFirstResponder);
2998 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
2999 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3000 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3001 UIScene* scene = scenes.anyObject;
3002 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3003 UIWindowScene* windowScene = (UIWindowScene*)scene;
3004 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3005 UIWindow* window = windowScene.windows[0];
3006 [window addSubview:viewController.view];
3008 [viewController loadView];
3011 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3013 [inputView setTextInputClient:123];
3014 [inputView reloadInputViews];
3015 [inputView becomeFirstResponder];
3018 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3019 [subView removeFromSuperview];
3023 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3024 [NSNotificationCenter.defaultCenter
3025 postNotificationName:UIKeyboardWillShowNotification
3027 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3031 [textInputPlugin handleMethodCall:onPointerMoveCall
3032 result:^(id _Nullable result){
3035 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3036 [subView removeFromSuperview];
3041 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3042 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3043 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3044 UIScene* scene = scenes.anyObject;
3045 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3046 UIWindowScene* windowScene = (UIWindowScene*)scene;
3047 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3048 UIWindow* window = windowScene.windows[0];
3049 [window addSubview:viewController.view];
3051 [viewController loadView];
3054 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3056 [inputView setTextInputClient:123];
3057 [inputView reloadInputViews];
3058 [inputView becomeFirstResponder];
3060 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3061 [NSNotificationCenter.defaultCenter
3062 postNotificationName:UIKeyboardWillShowNotification
3064 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3068 [textInputPlugin handleMethodCall:onPointerMoveCall
3069 result:^(id _Nullable result){
3073 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3078 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3079 result:^(id _Nullable result){
3083 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3085 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3086 [subView removeFromSuperview];
3091 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3092 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3093 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3094 UIScene* scene = scenes.anyObject;
3095 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3096 UIWindowScene* windowScene = (UIWindowScene*)scene;
3097 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3098 UIWindow* window = windowScene.windows[0];
3099 [window addSubview:viewController.view];
3101 [viewController loadView];
3104 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3106 [inputView setTextInputClient:123];
3107 [inputView reloadInputViews];
3108 [inputView becomeFirstResponder];
3110 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3111 [NSNotificationCenter.defaultCenter
3112 postNotificationName:UIKeyboardWillShowNotification
3114 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3118 [textInputPlugin handleMethodCall:onPointerMoveCall
3119 result:^(id _Nullable result){
3122 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3127 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3128 result:^(id _Nullable result){
3131 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3136 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3137 result:^(id _Nullable result){
3140 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3141 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3142 [subView removeFromSuperview];
3147 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3149 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3150 [inputView setTextInputClient:123];
3151 [inputView reloadInputViews];
3152 [inputView becomeFirstResponder];
3154 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3155 XCTAssertEqualObjects(inputView, firstResponder);
3159 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3166 [subInputView addSubview:subFirstResponderInputView];
3167 [inputView addSubview:subInputView];
3168 [inputView addSubview:otherSubInputView];
3169 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3170 [inputView setTextInputClient:123];
3171 [inputView reloadInputViews];
3172 [subInputView setTextInputClient:123];
3173 [subInputView reloadInputViews];
3174 [otherSubInputView setTextInputClient:123];
3175 [otherSubInputView reloadInputViews];
3176 [subFirstResponderInputView setTextInputClient:123];
3177 [subFirstResponderInputView reloadInputViews];
3178 [subFirstResponderInputView becomeFirstResponder];
3180 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3181 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3185 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3187 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3188 [inputView setTextInputClient:123];
3189 [inputView reloadInputViews];
3191 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3192 XCTAssertNil(firstResponder);
3196 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3197 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3198 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3199 UIScene* scene = scenes.anyObject;
3200 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3201 UIWindowScene* windowScene = (UIWindowScene*)scene;
3202 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3203 UIWindow* window = windowScene.windows[0];
3204 [window addSubview:viewController.view];
3206 [viewController loadView];
3208 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3209 initWithDescription:
3210 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3211 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3212 .andDo(^(NSInvocation* invocation) {
3213 [expectation fulfill];
3215 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3216 [NSNotificationCenter.defaultCenter
3217 postNotificationName:UIKeyboardWillShowNotification
3219 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3223 [textInputPlugin handleMethodCall:initialMoveCall
3224 result:^(id _Nullable result){
3229 [textInputPlugin handleMethodCall:subsequentMoveCall
3230 result:^(id _Nullable result){
3236 [textInputPlugin handleMethodCall:pointerUpCall
3237 result:^(id _Nullable result){
3240 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3244 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3245 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3246 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3247 UIScene* scene = scenes.anyObject;
3248 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3249 UIWindowScene* windowScene = (UIWindowScene*)scene;
3250 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3251 UIWindow* window = windowScene.windows[0];
3252 [window addSubview:viewController.view];
3254 [viewController loadView];
3256 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3257 [NSNotificationCenter.defaultCenter
3258 postNotificationName:UIKeyboardWillShowNotification
3260 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3264 [textInputPlugin handleMethodCall:initialMoveCall
3265 result:^(id _Nullable result){
3270 [textInputPlugin handleMethodCall:subsequentMoveCall
3271 result:^(id _Nullable result){
3277 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3278 result:^(id _Nullable result){
3284 [textInputPlugin handleMethodCall:pointerUpCall
3285 result:^(id _Nullable result){
3287 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3288 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3290 XCTNSPredicateExpectation* expectation =
3291 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3292 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3296 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3297 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3298 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3299 UIScene* scene = scenes.anyObject;
3300 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3301 UIWindowScene* windowScene = (UIWindowScene*)scene;
3302 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3303 UIWindow* window = windowScene.windows[0];
3304 [window addSubview:viewController.view];
3306 [viewController loadView];
3309 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3311 [inputView setTextInputClient:123];
3312 [inputView reloadInputViews];
3313 [inputView becomeFirstResponder];
3315 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3316 [NSNotificationCenter.defaultCenter
3317 postNotificationName:UIKeyboardWillShowNotification
3319 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3323 [textInputPlugin handleMethodCall:initialMoveCall
3324 result:^(id _Nullable result){
3329 [textInputPlugin handleMethodCall:subsequentMoveCall
3330 result:^(id _Nullable result){
3336 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3337 result:^(id _Nullable result){
3343 [textInputPlugin handleMethodCall:pointerUpCall
3344 result:^(id _Nullable result){
3346 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3347 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3349 XCTNSPredicateExpectation* expectation =
3350 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3351 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3355 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3356 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3357 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3358 UIScene* scene = scenes.anyObject;
3359 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3360 UIWindowScene* windowScene = (UIWindowScene*)scene;
3361 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3362 UIWindow* window = windowScene.windows[0];
3363 [window addSubview:viewController.view];
3365 [viewController loadView];
3367 XCTestExpectation* expectation =
3368 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3369 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3370 [NSNotificationCenter.defaultCenter
3371 postNotificationName:UIKeyboardWillShowNotification
3373 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3377 [textInputPlugin handleMethodCall:initialMoveCall
3378 result:^(id _Nullable result){
3383 [textInputPlugin handleMethodCall:subsequentMoveCall
3384 result:^(id _Nullable result){
3389 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3390 result:^(id _Nullable result){
3397 handleMethodCall:pointerUpCall
3398 result:^(id _Nullable result) {
3399 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3400 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3401 keyboardFrame.origin.y);
3402 [expectation fulfill];
3407 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3408 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3409 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3410 UIScene* scene = scenes.anyObject;
3411 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3412 UIWindowScene* windowScene = (UIWindowScene*)scene;
3413 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3414 UIWindow* window = windowScene.windows[0];
3415 [window addSubview:viewController.view];
3417 [viewController loadView];
3419 XCTestExpectation* expectation =
3420 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3421 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3422 [NSNotificationCenter.defaultCenter
3423 postNotificationName:UIKeyboardWillShowNotification
3425 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3429 [textInputPlugin handleMethodCall:initialMoveCall
3430 result:^(id _Nullable result){
3435 [textInputPlugin handleMethodCall:subsequentMoveCall
3436 result:^(id _Nullable result){
3443 handleMethodCall:pointerUpCall
3444 result:^(id _Nullable result) {
3445 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3446 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3447 [expectation fulfill];
3451 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3452 [UIView setAnimationsEnabled:YES];
3453 [textInputPlugin showKeyboardAndRemoveScreenshot];
3455 UIView.areAnimationsEnabled,
3456 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3459 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3460 [UIView setAnimationsEnabled:YES];
3461 [textInputPlugin showKeyboardAndRemoveScreenshot];
3463 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3465 return UIView.areAnimationsEnabled;
3467 XCTNSPredicateExpectation* expectation =
3468 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3469 [
self waitForExpectations:@[ expectation ] timeout:10.0];