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)testDeletingBackward {
531 NSDictionary* config =
self.mutableTemplateCopy;
532 [
self setClientId:123 configuration:config];
533 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
536 [inputView insertText:@"� ��� ��� text � ���� ���� ��� ���� ��� ���� ��� ���� ���� ���� ��� �� "];
537 [inputView deleteBackward];
538 [inputView deleteBackward];
541 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
542 [inputView deleteBackward];
543 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
544 [inputView deleteBackward];
545 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
546 [inputView deleteBackward];
547 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
548 [inputView deleteBackward];
550 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
551 [inputView deleteBackward];
552 [inputView deleteBackward];
553 [inputView deleteBackward];
554 [inputView deleteBackward];
555 [inputView deleteBackward];
556 [inputView deleteBackward];
558 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
559 [inputView deleteBackward];
560 XCTAssertEqualObjects(inputView.text,
@"ឹ");
561 [inputView deleteBackward];
562 XCTAssertEqualObjects(inputView.text,
@"");
567 - (void)testSystemOnlyAddingPartialComposedCharacter {
568 NSDictionary* config =
self.mutableTemplateCopy;
569 [
self setClientId:123 configuration:config];
570 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
573 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
574 [inputView deleteBackward];
577 [inputView insertText:[@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)]];
578 [inputView insertText:@"� ��"];
580 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
583 [inputView deleteBackward];
586 [inputView insertText:@"� ���"];
587 [inputView deleteBackward];
589 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
590 [inputView insertText:@"� ��"];
591 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
594 [inputView deleteBackward];
597 [inputView deleteBackward];
599 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
600 [inputView insertText:@"� ��"];
602 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
605 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
606 NSDictionary* config =
self.mutableTemplateCopy;
607 [
self setClientId:123 configuration:config];
608 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
611 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
612 [inputView deleteBackward];
613 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
616 NSString* brokenEmoji = [@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)];
617 [inputView insertText:brokenEmoji];
618 [inputView insertText:@"� ��"];
620 NSString* finalText = [NSString stringWithFormat:@"%@� ��", brokenEmoji];
621 XCTAssertEqualObjects(inputView.text, finalText);
624 - (void)testPastingNonTextDisallowed {
625 NSDictionary* config =
self.mutableTemplateCopy;
626 [
self setClientId:123 configuration:config];
627 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
630 UIPasteboard.generalPasteboard.color = UIColor.redColor;
631 XCTAssertNil(UIPasteboard.generalPasteboard.string);
632 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
633 [inputView paste:nil];
635 XCTAssertEqualObjects(inputView.text,
@"");
638 - (void)testNoZombies {
645 [passwordView.textField description];
647 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
650 - (void)testInputViewCrash {
655 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
656 activeView = inputPlugin.activeView;
658 [activeView updateEditingState];
661 - (void)testDoNotReuseInputViews {
662 NSDictionary* config =
self.mutableTemplateCopy;
663 [
self setClientId:123 configuration:config];
665 [
self setClientId:456 configuration:config];
667 XCTAssertNotNil(currentView);
672 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
674 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
678 - (void)testPropagatePressEventsToViewController {
680 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
681 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
685 NSDictionary* config =
self.mutableTemplateCopy;
686 [
self setClientId:123 configuration:config];
688 [
self setTextInputShow];
690 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
691 withEvent:OCMClassMock([UIPressesEvent class])];
693 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
694 withEvent:[OCMArg isNotNil]]);
695 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
696 withEvent:[OCMArg isNotNil]]);
698 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
699 withEvent:OCMClassMock([UIPressesEvent class])];
701 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
702 withEvent:[OCMArg isNotNil]]);
703 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
704 withEvent:[OCMArg isNotNil]]);
707 - (void)testPropagatePressEventsToViewController2 {
709 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
710 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
714 NSDictionary* config =
self.mutableTemplateCopy;
715 [
self setClientId:123 configuration:config];
716 [
self setTextInputShow];
719 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
720 withEvent:OCMClassMock([UIPressesEvent class])];
722 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
723 withEvent:[OCMArg isNotNil]]);
724 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
725 withEvent:[OCMArg isNotNil]]);
728 [
self setClientId:321 configuration:config];
729 [
self setTextInputShow];
731 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
733 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
734 withEvent:OCMClassMock([UIPressesEvent class])];
736 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
737 withEvent:[OCMArg isNotNil]]);
738 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
739 withEvent:[OCMArg isNotNil]]);
742 - (void)testUpdateSecureTextEntry {
743 NSDictionary* config =
self.mutableTemplateCopy;
744 [config setValue:@"YES" forKey:@"obscureText"];
745 [
self setClientId:123 configuration:config];
747 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
750 __block
int callCount = 0;
751 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
755 XCTAssertTrue(inputView.isSecureTextEntry);
757 config =
self.mutableTemplateCopy;
758 [config setValue:@"NO" forKey:@"obscureText"];
759 [
self updateConfig:config];
761 XCTAssertEqual(callCount, 1);
762 XCTAssertFalse(inputView.isSecureTextEntry);
765 - (void)testInputActionContinueAction {
781 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
783 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
786 - (void)testDisablingAutocorrectDisablesSpellChecking {
790 NSDictionary* config =
self.mutableTemplateCopy;
791 [inputView configureWithDictionary:config];
793 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
794 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
796 [config setValue:@(NO) forKey:@"autocorrect"];
797 [inputView configureWithDictionary:config];
799 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
800 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
803 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
805 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
819 XCTAssertEqual(inputView.markedTextRange, nil);
822 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
824 BOOL respondsToInsertionPointColor =
825 [inputView respondsToSelector:@selector(insertionPointColor)];
826 if (@available(iOS 17, *)) {
827 XCTAssertFalse(respondsToInsertionPointColor);
829 XCTAssertTrue(respondsToInsertionPointColor);
833 #pragma mark - TextEditingDelta tests
834 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
836 inputView.enableDeltaModel = YES;
838 __block
int updateCount = 0;
840 [inputView insertText:@"text to insert"];
843 flutterTextInputView:inputView
844 updateEditingClient:0
845 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
846 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
847 isEqualToString:
@""]) &&
848 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
849 isEqualToString:
@"text to insert"]) &&
850 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
851 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
853 .andDo(^(NSInvocation* invocation) {
856 XCTAssertEqual(updateCount, 0);
858 [
self flushScheduledAsyncBlocks];
861 XCTAssertEqual(updateCount, 1);
863 [inputView deleteBackward];
864 OCMExpect([
engine flutterTextInputView:inputView
865 updateEditingClient:0
866 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
867 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
868 isEqualToString:
@"text to insert"]) &&
869 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
870 isEqualToString:
@""]) &&
871 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
873 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
876 .andDo(^(NSInvocation* invocation) {
879 [
self flushScheduledAsyncBlocks];
880 XCTAssertEqual(updateCount, 2);
883 OCMExpect([
engine flutterTextInputView:inputView
884 updateEditingClient:0
885 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
886 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
887 isEqualToString:
@"text to inser"]) &&
888 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
889 isEqualToString:
@""]) &&
890 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
892 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
895 .andDo(^(NSInvocation* invocation) {
898 [
self flushScheduledAsyncBlocks];
899 XCTAssertEqual(updateCount, 3);
902 withText:@"replace text"];
905 flutterTextInputView:inputView
906 updateEditingClient:0
907 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
908 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
909 isEqualToString:
@"text to inser"]) &&
910 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
911 isEqualToString:
@"replace text"]) &&
912 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
913 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
915 .andDo(^(NSInvocation* invocation) {
918 [
self flushScheduledAsyncBlocks];
919 XCTAssertEqual(updateCount, 4);
921 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
922 OCMExpect([
engine flutterTextInputView:inputView
923 updateEditingClient:0
924 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
925 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
926 isEqualToString:
@"replace textext to inser"]) &&
927 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
928 isEqualToString:
@"marked text"]) &&
929 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
931 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
934 .andDo(^(NSInvocation* invocation) {
937 [
self flushScheduledAsyncBlocks];
938 XCTAssertEqual(updateCount, 5);
940 [inputView unmarkText];
942 flutterTextInputView:inputView
943 updateEditingClient:0
944 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
945 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
946 isEqualToString:
@"replace textmarked textext to inser"]) &&
947 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
948 isEqualToString:
@""]) &&
949 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
951 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
954 .andDo(^(NSInvocation* invocation) {
957 [
self flushScheduledAsyncBlocks];
959 XCTAssertEqual(updateCount, 6);
963 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
966 inputView.enableDeltaModel = YES;
969 OCMExpect([
engine flutterTextInputView:inputView
970 updateEditingClient:0
971 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
972 NSArray* deltas = state[@"deltas"];
973 NSDictionary* firstDelta = deltas[0];
974 NSDictionary* secondDelta = deltas[1];
975 NSDictionary* thirdDelta = deltas[2];
976 return [firstDelta[@"oldText"] isEqualToString:@""] &&
977 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
978 [firstDelta[@"deltaStart"] intValue] == 0 &&
979 [firstDelta[@"deltaEnd"] intValue] == 0 &&
980 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
981 [secondDelta[@"deltaText"] isEqualToString:@""] &&
982 [secondDelta[@"deltaStart"] intValue] == 0 &&
983 [secondDelta[@"deltaEnd"] intValue] == 1 &&
984 [thirdDelta[@"oldText"] isEqualToString:@""] &&
985 [thirdDelta[@"deltaText"] isEqualToString:@"� ��"] &&
986 [thirdDelta[@"deltaStart"] intValue] == 0 &&
987 [thirdDelta[@"deltaEnd"] intValue] == 0;
991 [inputView insertText:@"-"];
992 [inputView deleteBackward];
993 [inputView insertText:@"� ��"];
995 [
self flushScheduledAsyncBlocks];
999 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1001 inputView.enableDeltaModel = YES;
1003 __block
int updateCount = 0;
1004 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1005 .andDo(^(NSInvocation* invocation) {
1009 [inputView.text setString:@"Some initial text"];
1010 XCTAssertEqual(updateCount, 0);
1013 inputView.markedTextRange = range;
1014 inputView.selectedTextRange = nil;
1015 [
self flushScheduledAsyncBlocks];
1016 XCTAssertEqual(updateCount, 1);
1018 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1020 flutterTextInputView:inputView
1021 updateEditingClient:0
1022 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1023 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1024 isEqualToString:
@"Some initial text"]) &&
1025 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1026 isEqualToString:
@"new marked text."]) &&
1027 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1028 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1030 [
self flushScheduledAsyncBlocks];
1031 XCTAssertEqual(updateCount, 2);
1034 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1036 inputView.enableDeltaModel = YES;
1038 __block
int updateCount = 0;
1039 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1040 .andDo(^(NSInvocation* invocation) {
1044 [inputView.text setString:@"Some initial text"];
1045 [
self flushScheduledAsyncBlocks];
1046 XCTAssertEqual(updateCount, 0);
1049 inputView.markedTextRange = range;
1050 inputView.selectedTextRange = nil;
1051 [
self flushScheduledAsyncBlocks];
1052 XCTAssertEqual(updateCount, 1);
1054 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1056 flutterTextInputView:inputView
1057 updateEditingClient:0
1058 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1059 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1060 isEqualToString:
@"Some initial text"]) &&
1061 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1062 isEqualToString:
@"text."]) &&
1063 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1064 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1066 [
self flushScheduledAsyncBlocks];
1067 XCTAssertEqual(updateCount, 2);
1070 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1072 inputView.enableDeltaModel = YES;
1074 __block
int updateCount = 0;
1075 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1076 .andDo(^(NSInvocation* invocation) {
1080 [inputView.text setString:@"Some initial text"];
1081 [
self flushScheduledAsyncBlocks];
1082 XCTAssertEqual(updateCount, 0);
1085 inputView.markedTextRange = range;
1086 inputView.selectedTextRange = nil;
1087 [
self flushScheduledAsyncBlocks];
1088 XCTAssertEqual(updateCount, 1);
1090 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1092 flutterTextInputView:inputView
1093 updateEditingClient:0
1094 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1095 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1096 isEqualToString:
@"Some initial text"]) &&
1097 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1098 isEqualToString:
@"tex"]) &&
1099 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1100 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1102 [
self flushScheduledAsyncBlocks];
1103 XCTAssertEqual(updateCount, 2);
1106 #pragma mark - EditingState tests
1108 - (void)testUITextInputCallsUpdateEditingStateOnce {
1111 __block
int updateCount = 0;
1112 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1113 .andDo(^(NSInvocation* invocation) {
1117 [inputView insertText:@"text to insert"];
1119 XCTAssertEqual(updateCount, 1);
1121 [inputView deleteBackward];
1122 XCTAssertEqual(updateCount, 2);
1125 XCTAssertEqual(updateCount, 3);
1128 withText:@"replace text"];
1129 XCTAssertEqual(updateCount, 4);
1131 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1132 XCTAssertEqual(updateCount, 5);
1134 [inputView unmarkText];
1135 XCTAssertEqual(updateCount, 6);
1138 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1140 inputView.enableDeltaModel = YES;
1142 __block
int updateCount = 0;
1143 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1144 .andDo(^(NSInvocation* invocation) {
1148 [inputView insertText:@"text to insert"];
1149 [
self flushScheduledAsyncBlocks];
1151 XCTAssertEqual(updateCount, 1);
1153 [inputView deleteBackward];
1154 [
self flushScheduledAsyncBlocks];
1155 XCTAssertEqual(updateCount, 2);
1158 [
self flushScheduledAsyncBlocks];
1159 XCTAssertEqual(updateCount, 3);
1162 withText:@"replace text"];
1163 [
self flushScheduledAsyncBlocks];
1164 XCTAssertEqual(updateCount, 4);
1166 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1167 [
self flushScheduledAsyncBlocks];
1168 XCTAssertEqual(updateCount, 5);
1170 [inputView unmarkText];
1171 [
self flushScheduledAsyncBlocks];
1172 XCTAssertEqual(updateCount, 6);
1175 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1178 __block
int updateCount = 0;
1179 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1180 .andDo(^(NSInvocation* invocation) {
1184 [inputView.text setString:@"BEFORE"];
1185 XCTAssertEqual(updateCount, 0);
1187 inputView.markedTextRange = nil;
1188 inputView.selectedTextRange = nil;
1189 XCTAssertEqual(updateCount, 1);
1192 XCTAssertEqual(updateCount, 1);
1193 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1194 XCTAssertEqual(updateCount, 1);
1195 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1196 XCTAssertEqual(updateCount, 1);
1200 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1201 XCTAssertEqual(updateCount, 1);
1203 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1204 XCTAssertEqual(updateCount, 1);
1208 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1209 XCTAssertEqual(updateCount, 1);
1211 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1212 XCTAssertEqual(updateCount, 1);
1215 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1217 inputView.enableDeltaModel = YES;
1219 __block
int updateCount = 0;
1220 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1221 .andDo(^(NSInvocation* invocation) {
1225 [inputView.text setString:@"BEFORE"];
1226 [
self flushScheduledAsyncBlocks];
1227 XCTAssertEqual(updateCount, 0);
1229 inputView.markedTextRange = nil;
1230 inputView.selectedTextRange = nil;
1231 [
self flushScheduledAsyncBlocks];
1232 XCTAssertEqual(updateCount, 1);
1235 XCTAssertEqual(updateCount, 1);
1236 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1237 [
self flushScheduledAsyncBlocks];
1238 XCTAssertEqual(updateCount, 1);
1240 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1241 [
self flushScheduledAsyncBlocks];
1242 XCTAssertEqual(updateCount, 1);
1246 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1247 [
self flushScheduledAsyncBlocks];
1248 XCTAssertEqual(updateCount, 1);
1251 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1252 [
self flushScheduledAsyncBlocks];
1253 XCTAssertEqual(updateCount, 1);
1257 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1258 [
self flushScheduledAsyncBlocks];
1259 XCTAssertEqual(updateCount, 1);
1262 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1263 [
self flushScheduledAsyncBlocks];
1264 XCTAssertEqual(updateCount, 1);
1267 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1270 __block
int updateCount = 0;
1271 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1272 .andDo(^(NSInvocation* invocation) {
1276 [inputView unmarkText];
1278 XCTAssertEqual(updateCount, 0);
1280 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1282 XCTAssertEqual(updateCount, 1);
1284 [inputView unmarkText];
1286 XCTAssertEqual(updateCount, 2);
1289 - (void)testCanCopyPasteWithScribbleEnabled {
1290 if (@available(iOS 14.0, *)) {
1291 NSDictionary* config =
self.mutableTemplateCopy;
1292 [
self setClientId:123 configuration:config];
1293 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1299 [mockInputView insertText:@"aaaa"];
1300 [mockInputView selectAll:nil];
1302 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1303 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1304 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1305 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1307 [mockInputView copy:NULL];
1308 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1309 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1310 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1311 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1315 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1316 if (@available(iOS 14.0, *)) {
1319 __block
int updateCount = 0;
1320 OCMStub([
engine flutterTextInputView:inputView
1321 updateEditingClient:0
1322 withState:[OCMArg isNotNil]])
1323 .andDo(^(NSInvocation* invocation) {
1327 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1329 XCTAssertEqual(updateCount, 1);
1331 UIScribbleInteraction* scribbleInteraction =
1332 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1334 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1335 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1337 XCTAssertEqual(updateCount, 1);
1339 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1340 [inputView resetScribbleInteractionStatusIfEnding];
1341 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1343 XCTAssertEqual(updateCount, 2);
1345 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1346 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1349 XCTAssertEqual(updateCount, 2);
1351 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1352 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1355 XCTAssertEqual(updateCount, 2);
1357 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1358 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1360 XCTAssertEqual(updateCount, 3);
1364 - (void)testUpdateEditingClientNegativeSelection {
1367 [inputView.text setString:@"SELECTION"];
1368 inputView.markedTextRange = nil;
1369 inputView.selectedTextRange = nil;
1371 [inputView setTextInputState:@{
1372 @"text" : @"SELECTION",
1373 @"selectionBase" : @-1,
1374 @"selectionExtent" : @-1
1376 [inputView updateEditingState];
1377 OCMVerify([
engine flutterTextInputView:inputView
1378 updateEditingClient:0
1379 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1380 return ([state[
@"selectionBase"] intValue]) == 0 &&
1381 ([state[
@"selectionExtent"] intValue] == 0);
1386 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1387 [inputView updateEditingState];
1388 OCMVerify([
engine flutterTextInputView:inputView
1389 updateEditingClient:0
1390 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1391 return ([state[
@"selectionBase"] intValue]) == 0 &&
1392 ([state[
@"selectionExtent"] intValue] == 0);
1396 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1397 [inputView updateEditingState];
1398 OCMVerify([
engine flutterTextInputView:inputView
1399 updateEditingClient:0
1400 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1401 return ([state[
@"selectionBase"] intValue]) == 0 &&
1402 ([state[
@"selectionExtent"] intValue] == 0);
1406 - (void)testUpdateEditingClientSelectionClamping {
1410 [inputView.text setString:@"SELECTION"];
1411 inputView.markedTextRange = nil;
1412 inputView.selectedTextRange = nil;
1415 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1416 [inputView updateEditingState];
1417 OCMVerify([
engine flutterTextInputView:inputView
1418 updateEditingClient:0
1419 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1420 return ([state[
@"selectionBase"] intValue]) == 0 &&
1421 ([state[
@"selectionExtent"] intValue] == 0);
1425 [inputView setTextInputState:@{
1426 @"text" : @"SELECTION",
1427 @"selectionBase" : @0,
1428 @"selectionExtent" : @9999
1430 [inputView updateEditingState];
1432 OCMVerify([
engine flutterTextInputView:inputView
1433 updateEditingClient:0
1434 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1435 return ([state[
@"selectionBase"] intValue]) == 0 &&
1436 ([state[
@"selectionExtent"] intValue] == 9);
1441 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1442 [inputView updateEditingState];
1443 OCMVerify([
engine flutterTextInputView:inputView
1444 updateEditingClient:0
1445 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1446 return ([state[
@"selectionBase"] intValue]) == 0 &&
1447 ([state[
@"selectionExtent"] intValue] == 1);
1451 [inputView setTextInputState:@{
1452 @"text" : @"SELECTION",
1453 @"selectionBase" : @9999,
1454 @"selectionExtent" : @9999
1456 [inputView updateEditingState];
1457 OCMVerify([
engine flutterTextInputView:inputView
1458 updateEditingClient:0
1459 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1460 return ([state[
@"selectionBase"] intValue]) == 9 &&
1461 ([state[
@"selectionExtent"] intValue] == 9);
1465 - (void)testInputViewsHasNonNilInputDelegate {
1466 if (@available(iOS 13.0, *)) {
1468 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1470 [inputView setTextInputClient:123];
1471 [inputView reloadInputViews];
1472 [inputView becomeFirstResponder];
1473 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1474 inputView.inputDelegate = nil;
1477 [mockInputView setTextInputState:@{
1478 @"text" : @"COMPOSING",
1479 @"composingBase" : @1,
1480 @"composingExtent" : @3
1482 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1483 [inputView removeFromSuperview];
1487 - (void)testInputViewsDoNotHaveUITextInteractions {
1488 if (@available(iOS 13.0, *)) {
1490 BOOL hasTextInteraction = NO;
1491 for (
id interaction in inputView.interactions) {
1492 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1493 if (hasTextInteraction) {
1497 XCTAssertFalse(hasTextInteraction);
1501 #pragma mark - UITextInput methods - Tests
1503 - (void)testUpdateFirstRectForRange {
1504 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1510 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1515 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1516 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1520 NSArray* affineMatrix = @[
1521 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1522 @(-6.0), @(3.0), @(9.0), @(1.0)
1526 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1528 [inputView setEditableTransform:yOffsetMatrix];
1530 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1533 CGRect testRect = CGRectMake(0, 0, 100, 100);
1534 [inputView setMarkedRect:testRect];
1536 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1537 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1539 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1542 [inputView setEditableTransform:zeroMatrix];
1544 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1545 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1548 [inputView setEditableTransform:yOffsetMatrix];
1549 [inputView setMarkedRect:testRect];
1550 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1553 [inputView setMarkedRect:kInvalidFirstRect];
1555 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1556 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1559 [inputView setEditableTransform:affineMatrix];
1560 [inputView setMarkedRect:testRect];
1562 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1564 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1565 const CGPoint offset = CGPointMake(113, 119);
1566 CGRect currentFrame = inputView.frame;
1567 currentFrame.origin = offset;
1568 inputView.frame = currentFrame;
1571 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1572 [inputView firstRectForRange:range]));
1575 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1577 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1582 [inputView setSelectionRects:@[
1591 if (@available(iOS 17, *)) {
1592 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1593 [inputView firstRectForRange:multiRectRange]));
1595 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1596 [inputView firstRectForRange:multiRectRange]));
1600 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1602 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1604 [inputView setSelectionRects:@[
1611 if (@available(iOS 17, *)) {
1612 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1613 [inputView firstRectForRange:singleRectRange]));
1615 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1620 if (@available(iOS 17, *)) {
1621 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1622 [inputView firstRectForRange:multiRectRange]));
1624 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1627 [inputView setTextInputState:@{@"text" : @"COM"}];
1629 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1632 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1634 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1636 [inputView setSelectionRects:@[
1643 if (@available(iOS 17, *)) {
1644 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1645 [inputView firstRectForRange:singleRectRange]));
1647 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1651 if (@available(iOS 17, *)) {
1652 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1653 [inputView firstRectForRange:multiRectRange]));
1655 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1658 [inputView setTextInputState:@{@"text" : @"COM"}];
1660 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1663 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1665 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1667 [inputView setSelectionRects:@[
1678 if (@available(iOS 17, *)) {
1679 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1680 [inputView firstRectForRange:singleRectRange]));
1682 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1687 if (@available(iOS 17, *)) {
1688 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1689 [inputView firstRectForRange:multiRectRange]));
1691 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1695 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1697 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1699 [inputView setSelectionRects:@[
1710 if (@available(iOS 17, *)) {
1711 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1712 [inputView firstRectForRange:singleRectRange]));
1714 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1718 if (@available(iOS 17, *)) {
1719 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1720 [inputView firstRectForRange:multiRectRange]));
1722 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1726 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1728 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1730 [inputView setSelectionRects:@[
1741 if (@available(iOS 17, *)) {
1742 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1743 [inputView firstRectForRange:multiRectRange]));
1745 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1749 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1751 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1753 [inputView setSelectionRects:@[
1764 if (@available(iOS 17, *)) {
1765 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1766 [inputView firstRectForRange:multiRectRange]));
1768 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1772 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1774 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1776 [inputView setSelectionRects:@[
1787 if (@available(iOS 17, *)) {
1788 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1789 [inputView firstRectForRange:multiRectRange]));
1791 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1795 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1797 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1799 [inputView setSelectionRects:@[
1810 if (@available(iOS 17, *)) {
1811 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1812 [inputView firstRectForRange:multiRectRange]));
1814 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1818 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1820 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1822 [inputView setSelectionRects:@[
1833 if (@available(iOS 17, *)) {
1834 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1835 [inputView firstRectForRange:multiRectRange]));
1837 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1841 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1843 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1845 [inputView setSelectionRects:@[
1856 if (@available(iOS 17, *)) {
1857 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1858 [inputView firstRectForRange:multiRectRange]));
1860 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1864 - (void)testClosestPositionToPoint {
1866 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1869 [inputView setSelectionRects:@[
1874 CGPoint point = CGPointMake(150, 150);
1875 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1876 XCTAssertEqual(UITextStorageDirectionBackward,
1881 [inputView setSelectionRects:@[
1888 point = CGPointMake(125, 150);
1889 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1890 XCTAssertEqual(UITextStorageDirectionForward,
1895 [inputView setSelectionRects:@[
1902 point = CGPointMake(125, 201);
1903 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1904 XCTAssertEqual(UITextStorageDirectionBackward,
1908 [inputView setSelectionRects:@[
1914 point = CGPointMake(125, 250);
1915 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1916 XCTAssertEqual(UITextStorageDirectionBackward,
1920 [inputView setSelectionRects:@[
1925 point = CGPointMake(110, 50);
1926 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1927 XCTAssertEqual(UITextStorageDirectionForward,
1932 [inputView beginFloatingCursorAtPoint:CGPointZero];
1933 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1934 XCTAssertEqual(UITextStorageDirectionForward,
1936 [inputView endFloatingCursor];
1939 - (void)testClosestPositionToPointRTL {
1941 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1943 [inputView setSelectionRects:@[
1959 XCTAssertEqual(0U, position.
index);
1960 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1962 XCTAssertEqual(1U, position.
index);
1963 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1965 XCTAssertEqual(1U, position.
index);
1966 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1968 XCTAssertEqual(2U, position.
index);
1969 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1971 XCTAssertEqual(2U, position.
index);
1972 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1974 XCTAssertEqual(3U, position.
index);
1975 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1977 XCTAssertEqual(3U, position.
index);
1978 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1981 - (void)testSelectionRectsForRange {
1983 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1985 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
1986 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
1987 [inputView setSelectionRects:@[
1996 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
1997 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
1998 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2002 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2003 XCTAssertTrue(CGRectEqualToRect(
2004 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2005 [inputView selectionRectsForRange:range][0].rect));
2008 - (void)testClosestPositionToPointWithinRange {
2010 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2013 [inputView setSelectionRects:@[
2020 CGPoint point = CGPointMake(125, 150);
2023 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2025 UITextStorageDirectionForward,
2026 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2029 [inputView setSelectionRects:@[
2036 point = CGPointMake(125, 150);
2039 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2041 UITextStorageDirectionForward,
2042 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2045 - (void)testClosestPositionToPointWithPartialSelectionRects {
2047 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2054 XCTAssertTrue(CGRectEqualToRect(
2057 affinity:UITextStorageDirectionForward]],
2058 CGRectMake(100, 0, 0, 100)));
2061 XCTAssertTrue(CGRectEqualToRect(
2064 affinity:UITextStorageDirectionForward]],
2068 #pragma mark - Floating Cursor - Tests
2070 - (void)testFloatingCursorDoesNotThrow {
2073 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2074 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2075 [inputView endFloatingCursor];
2076 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2077 [inputView endFloatingCursor];
2080 - (void)testFloatingCursor {
2082 [inputView setTextInputState:@{
2084 @"selectionBase" : @1,
2085 @"selectionExtent" : @1,
2096 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2099 XCTAssertTrue(CGRectEqualToRect(
2102 affinity:UITextStorageDirectionForward]],
2103 CGRectMake(0, 0, 0, 100)));
2106 XCTAssertTrue(CGRectEqualToRect(
2109 affinity:UITextStorageDirectionForward]],
2110 CGRectMake(100, 100, 0, 100)));
2111 XCTAssertTrue(CGRectEqualToRect(
2114 affinity:UITextStorageDirectionForward]],
2115 CGRectMake(200, 200, 0, 100)));
2116 XCTAssertTrue(CGRectEqualToRect(
2119 affinity:UITextStorageDirectionForward]],
2120 CGRectMake(300, 300, 0, 100)));
2123 XCTAssertTrue(CGRectEqualToRect(
2126 affinity:UITextStorageDirectionForward]],
2127 CGRectMake(400, 300, 0, 100)));
2129 XCTAssertTrue(CGRectEqualToRect(
2132 affinity:UITextStorageDirectionForward]],
2136 [inputView setTextInputState:@{
2138 @"selectionBase" : @2,
2139 @"selectionExtent" : @2,
2142 XCTAssertTrue(CGRectEqualToRect(
2145 affinity:UITextStorageDirectionBackward]],
2146 CGRectMake(0, 0, 0, 100)));
2149 XCTAssertTrue(CGRectEqualToRect(
2152 affinity:UITextStorageDirectionBackward]],
2153 CGRectMake(100, 0, 0, 100)));
2154 XCTAssertTrue(CGRectEqualToRect(
2157 affinity:UITextStorageDirectionBackward]],
2158 CGRectMake(200, 100, 0, 100)));
2159 XCTAssertTrue(CGRectEqualToRect(
2162 affinity:UITextStorageDirectionBackward]],
2163 CGRectMake(300, 200, 0, 100)));
2164 XCTAssertTrue(CGRectEqualToRect(
2167 affinity:UITextStorageDirectionBackward]],
2168 CGRectMake(400, 300, 0, 100)));
2170 XCTAssertTrue(CGRectEqualToRect(
2173 affinity:UITextStorageDirectionBackward]],
2178 CGRect initialBounds = inputView.bounds;
2179 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2180 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2181 OCMVerify([
engine flutterTextInputView:inputView
2182 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2184 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2185 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2186 ([state[
@"Y"] isEqualToNumber:@(0)]);
2189 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2190 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2191 OCMVerify([
engine flutterTextInputView:inputView
2192 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2194 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2195 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2196 ([state[
@"Y"] isEqualToNumber:@(333)]);
2199 [inputView endFloatingCursor];
2200 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2201 OCMVerify([
engine flutterTextInputView:inputView
2202 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2204 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2205 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2206 ([state[
@"Y"] isEqualToNumber:@(0)]);
2210 #pragma mark - UIKeyInput Overrides - Tests
2212 - (void)testInsertTextAddsPlaceholderSelectionRects {
2215 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2225 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2228 [inputView insertText:@"in"];
2256 #pragma mark - Autofill - Utilities
2258 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2261 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2262 @"keyboardAppearance" :
@"Brightness.light",
2263 @"obscureText" : @YES,
2264 @"inputAction" :
@"TextInputAction.unspecified",
2265 @"smartDashesType" :
@"0",
2266 @"smartQuotesType" :
@"0",
2267 @"autocorrect" : @YES
2271 return [_passwordTemplate mutableCopy];
2275 return [
self.installedInputViews
2276 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2279 - (void)commitAutofillContextAndVerify {
2283 [textInputPlugin handleMethodCall:methodCall
2284 result:^(id _Nullable result){
2287 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2292 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2296 #pragma mark - Autofill - Tests
2298 - (void)testDisablingAutofillOnInputClient {
2299 NSDictionary* config =
self.mutableTemplateCopy;
2300 [config setValue:@"YES" forKey:@"obscureText"];
2302 [
self setClientId:123 configuration:config];
2305 XCTAssertEqualObjects(inputView.textContentType,
@"");
2308 - (void)testAutofillEnabledByDefault {
2309 NSDictionary* config =
self.mutableTemplateCopy;
2310 [config setValue:@"NO" forKey:@"obscureText"];
2311 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2312 forKey:@"autofill"];
2314 [
self setClientId:123 configuration:config];
2317 XCTAssertNil(inputView.textContentType);
2320 - (void)testAutofillContext {
2321 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2324 @"uniqueIdentifier" : @"field1",
2325 @"hints" : @[ @"hint1" ],
2326 @"editingValue" : @{@"text" : @""}
2328 forKey:@"autofill"];
2330 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2332 @"uniqueIdentifier" : @"field2",
2333 @"hints" : @[ @"hint2" ],
2334 @"editingValue" : @{@"text" : @""}
2336 forKey:@"autofill"];
2338 NSMutableDictionary* config = [field1 mutableCopy];
2339 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2341 [
self setClientId:123 configuration:config];
2342 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2346 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2347 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2349 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2352 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2354 @"uniqueIdentifier" : @"field3",
2355 @"hints" : @[ @"hint3" ],
2356 @"editingValue" : @{@"text" : @""}
2358 forKey:@"autofill"];
2362 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2364 [
self setClientId:123 configuration:config];
2366 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2369 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2370 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2372 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2375 for (NSString* key in oldContext.allKeys) {
2376 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2380 config =
self.mutablePasswordTemplateCopy;
2383 [
self setClientId:124 configuration:config];
2384 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2386 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2389 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2390 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2393 for (NSString* key in oldContext.allKeys) {
2394 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2398 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2402 [
self setClientId:200 configuration:config];
2405 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2408 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2409 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2412 for (NSString* key in oldContext.allKeys) {
2413 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2416 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2419 - (void)testCommitAutofillContext {
2420 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2422 @"uniqueIdentifier" : @"field1",
2423 @"hints" : @[ @"hint1" ],
2424 @"editingValue" : @{@"text" : @""}
2426 forKey:@"autofill"];
2428 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2430 @"uniqueIdentifier" : @"field2",
2431 @"hints" : @[ @"hint2" ],
2432 @"editingValue" : @{@"text" : @""}
2434 forKey:@"autofill"];
2436 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2438 @"uniqueIdentifier" : @"field3",
2439 @"hints" : @[ @"hint3" ],
2440 @"editingValue" : @{@"text" : @""}
2442 forKey:@"autofill"];
2444 NSMutableDictionary* config = [field1 mutableCopy];
2445 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2447 [
self setClientId:123 configuration:config];
2448 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2450 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2452 [
self commitAutofillContextAndVerify];
2453 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2456 [
self setClientId:123 configuration:config];
2458 [
self setClientId:124 configuration:field3];
2459 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2461 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2462 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2465 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2467 [
self commitAutofillContextAndVerify];
2468 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2471 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2473 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2477 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2478 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2480 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2482 [
self commitAutofillContextAndVerify];
2483 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2486 - (void)testAutofillInputViews {
2487 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2489 @"uniqueIdentifier" : @"field1",
2490 @"hints" : @[ @"hint1" ],
2491 @"editingValue" : @{@"text" : @""}
2493 forKey:@"autofill"];
2495 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2497 @"uniqueIdentifier" : @"field2",
2498 @"hints" : @[ @"hint2" ],
2499 @"editingValue" : @{@"text" : @""}
2501 forKey:@"autofill"];
2503 NSMutableDictionary* config = [field1 mutableCopy];
2504 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2506 [
self setClientId:123 configuration:config];
2507 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2510 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2513 XCTAssertEqual(inputFields.count, 2ul);
2514 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2519 withText:@"Autofilled!"];
2520 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2523 OCMVerify([
engine flutterTextInputView:inactiveView
2524 updateEditingClient:0
2525 withState:[OCMArg isNotNil]
2526 withTag:
@"field2"]);
2529 - (void)testPasswordAutofillHack {
2530 NSDictionary* config =
self.mutableTemplateCopy;
2531 [config setValue:@"YES" forKey:@"obscureText"];
2532 [
self setClientId:123 configuration:config];
2535 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2539 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2542 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2545 - (void)testClearAutofillContextClearsSelection {
2546 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2547 NSDictionary* editingValue = @{
2548 @"text" :
@"REGULAR_TEXT_FIELD",
2549 @"composingBase" : @0,
2550 @"composingExtent" : @3,
2551 @"selectionBase" : @1,
2552 @"selectionExtent" : @4
2554 [regularField setValue:@{
2555 @"uniqueIdentifier" : @"field2",
2556 @"hints" : @[ @"hint2" ],
2557 @"editingValue" : editingValue,
2559 forKey:@"autofill"];
2560 [regularField addEntriesFromDictionary:editingValue];
2561 [
self setClientId:123 configuration:regularField];
2562 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2563 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2566 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2568 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2572 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2573 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2575 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2577 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2578 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2581 XCTAssert([oldInputView.text isEqualToString:
@""]);
2583 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2586 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2588 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2589 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2591 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2594 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2595 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2597 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2599 [
self commitAutofillContextAndVerify];
2602 - (void)testScribbleSetSelectionRects {
2603 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2604 NSDictionary* editingValue = @{
2605 @"text" :
@"REGULAR_TEXT_FIELD",
2606 @"composingBase" : @0,
2607 @"composingExtent" : @3,
2608 @"selectionBase" : @1,
2609 @"selectionExtent" : @4
2611 [regularField setValue:@{
2612 @"uniqueIdentifier" : @"field1",
2613 @"hints" : @[ @"hint2" ],
2614 @"editingValue" : editingValue,
2616 forKey:@"autofill"];
2617 [regularField addEntriesFromDictionary:editingValue];
2618 [
self setClientId:123 configuration:regularField];
2619 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2620 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2622 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2623 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2627 [textInputPlugin handleMethodCall:methodCall
2628 result:^(id _Nullable result){
2631 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2634 - (void)testDecommissionedViewAreNotReusedByAutofill {
2636 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2637 [configuration setValue:@{
2638 @"uniqueIdentifier" : @"field1",
2639 @"hints" : @[ UITextContentTypePassword ],
2640 @"editingValue" : @{@"text" : @""}
2642 forKey:@"autofill"];
2643 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2645 [
self setClientId:123 configuration:configuration];
2647 [
self setTextInputHide];
2650 [
self setClientId:124 configuration:configuration];
2654 XCTAssertNotNil(previousActiveView);
2658 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2665 #pragma mark - Accessibility - Tests
2667 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2668 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2671 [
self setTextInputShow];
2673 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2676 XCTAssertEqual([inputFields count], 1u);
2679 [
self setTextInputHide];
2681 inputFields =
self.installedInputViews;
2684 XCTAssertEqual([inputFields count], 0u);
2687 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2690 UIView* container = [[UIView alloc] init];
2691 UIAccessibilityElement* backing =
2692 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2693 inputView.backingTextInputAccessibilityObject = backing;
2696 [inputView accessibilityElementDidBecomeFocused];
2702 - (void)testFlutterTokenizerCanParseLines {
2704 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2707 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2708 XCTAssertEqual(range.
range.location, 0u);
2709 XCTAssertEqual(range.
range.length, 0u);
2711 [inputView insertText:@"how are you\nI am fine, Thank you"];
2713 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2714 XCTAssertEqual(range.
range.location, 0u);
2715 XCTAssertEqual(range.
range.length, 11u);
2717 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2718 XCTAssertEqual(range.
range.location, 0u);
2719 XCTAssertEqual(range.
range.length, 11u);
2721 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2722 XCTAssertEqual(range.
range.location, 0u);
2723 XCTAssertEqual(range.
range.length, 11u);
2725 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2726 XCTAssertEqual(range.
range.location, 12u);
2727 XCTAssertEqual(range.
range.length, 20u);
2729 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2730 XCTAssertEqual(range.
range.location, 12u);
2731 XCTAssertEqual(range.
range.length, 20u);
2733 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2734 XCTAssertEqual(range.
range.location, 12u);
2735 XCTAssertEqual(range.
range.length, 20u);
2738 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2740 [inputView insertText:@"0123456789\n012345"];
2741 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2744 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2745 withGranularity:UITextGranularityLine
2746 inDirection:UITextStorageDirectionBackward];
2747 XCTAssertEqual(range.
range.location, 11u);
2748 XCTAssertEqual(range.
range.length, 6u);
2751 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2753 [inputView insertText:@"0123456789\n012345"];
2754 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2757 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2758 withGranularity:UITextGranularityLine
2759 inDirection:UITextStorageDirectionForward];
2760 if (@available(iOS 17.0, *)) {
2761 XCTAssertNil(range);
2763 XCTAssertEqual(range.
range.location, 11u);
2764 XCTAssertEqual(range.
range.length, 6u);
2768 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2770 [inputView insertText:@"0123456789\n012345"];
2771 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2776 withGranularity:UITextGranularityLine
2777 inDirection:UITextStorageDirectionForward];
2778 if (@available(iOS 17.0, *)) {
2779 XCTAssertNil(range);
2781 XCTAssertEqual(range.
range.location, 0u);
2782 XCTAssertEqual(range.
range.length, 0u);
2786 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2791 __weak UIView* activeView;
2796 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2799 result:^(id _Nullable result){
2805 result:^(id _Nullable result){
2807 XCTAssertNotNil(activeView);
2810 XCTAssertNotNil(activeView);
2813 - (void)testFlutterTextInputPluginHostViewNilCrash {
2816 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2819 - (void)testFlutterTextInputPluginHostViewNotNil {
2825 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2828 - (void)testSetPlatformViewClient {
2835 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2837 result:^(id _Nullable result){
2840 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2843 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2845 result:^(id _Nullable result){
2847 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2850 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
2851 if (@available(iOS 16.0, *)) {
2853 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2854 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
2855 @"editMenuInteraction setup delegate correctly");
2859 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
2860 if (@available(iOS 16.0, *)) {
2864 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
2868 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
2869 if (@available(iOS 16.0, *)) {
2874 [myViewController loadView];
2877 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2879 result:^(id _Nullable result){
2885 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2887 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2888 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2890 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2891 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2892 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2893 .andDo(^(NSInvocation* invocation) {
2895 [invocation retainArguments];
2896 UIEditMenuConfiguration* config;
2897 [invocation getArgument:&config atIndex:2];
2898 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
2899 @"UIEditMenuConfiguration must use automatic arrow direction.");
2900 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
2901 @"UIEditMenuConfiguration must have the correct point.");
2902 [expectation fulfill];
2905 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2906 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
2908 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2909 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2910 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2914 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
2915 if (@available(iOS 16.0, *)) {
2920 [myViewController loadView];
2924 arguments:@[ @(123),
self.mutableTemplateCopy ]];
2926 result:^(id _Nullable result){
2932 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
2934 XCTestExpectation* expectation = [[XCTestExpectation alloc]
2935 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
2937 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
2938 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
2939 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
2940 .andDo(^(NSInvocation* invocation) {
2941 [expectation fulfill];
2944 myInputView.frame = CGRectMake(10, 20, 30, 40);
2945 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
2946 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
2948 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
2949 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
2950 [
self waitForExpectations:@[ expectation ] timeout:1.0];
2953 [myInputView editMenuInteraction:mockInteraction
2954 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
2956 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
2957 @"targetRectForConfiguration must return the correct target rect.");
2961 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
2963 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2965 [inputView setTextInputClient:123];
2966 [inputView reloadInputViews];
2967 [inputView becomeFirstResponder];
2968 XCTAssert(inputView.isFirstResponder);
2970 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2971 [NSNotificationCenter.defaultCenter
2972 postNotificationName:UIKeyboardWillShowNotification
2974 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2978 [textInputPlugin handleMethodCall:onPointerMoveCall
2979 result:^(id _Nullable result){
2981 XCTAssertFalse(inputView.isFirstResponder);
2985 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
2986 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2987 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2988 UIScene* scene = scenes.anyObject;
2989 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2990 UIWindowScene* windowScene = (UIWindowScene*)scene;
2991 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2992 UIWindow* window = windowScene.windows[0];
2993 [window addSubview:viewController.view];
2995 [viewController loadView];
2998 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3000 [inputView setTextInputClient:123];
3001 [inputView reloadInputViews];
3002 [inputView becomeFirstResponder];
3005 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3006 [subView removeFromSuperview];
3010 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3011 [NSNotificationCenter.defaultCenter
3012 postNotificationName:UIKeyboardWillShowNotification
3014 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3018 [textInputPlugin handleMethodCall:onPointerMoveCall
3019 result:^(id _Nullable result){
3022 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3023 [subView removeFromSuperview];
3028 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
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];
3047 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3048 [NSNotificationCenter.defaultCenter
3049 postNotificationName:UIKeyboardWillShowNotification
3051 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3055 [textInputPlugin handleMethodCall:onPointerMoveCall
3056 result:^(id _Nullable result){
3060 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3065 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3066 result:^(id _Nullable result){
3070 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3072 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3073 [subView removeFromSuperview];
3078 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3079 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3080 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3081 UIScene* scene = scenes.anyObject;
3082 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3083 UIWindowScene* windowScene = (UIWindowScene*)scene;
3084 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3085 UIWindow* window = windowScene.windows[0];
3086 [window addSubview:viewController.view];
3088 [viewController loadView];
3091 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3093 [inputView setTextInputClient:123];
3094 [inputView reloadInputViews];
3095 [inputView becomeFirstResponder];
3097 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3098 [NSNotificationCenter.defaultCenter
3099 postNotificationName:UIKeyboardWillShowNotification
3101 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3105 [textInputPlugin handleMethodCall:onPointerMoveCall
3106 result:^(id _Nullable result){
3109 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3114 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3115 result:^(id _Nullable result){
3118 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3123 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3124 result:^(id _Nullable result){
3127 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3128 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3129 [subView removeFromSuperview];
3134 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3136 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3137 [inputView setTextInputClient:123];
3138 [inputView reloadInputViews];
3139 [inputView becomeFirstResponder];
3141 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3142 XCTAssertEqualObjects(inputView, firstResponder);
3146 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3153 [subInputView addSubview:subFirstResponderInputView];
3154 [inputView addSubview:subInputView];
3155 [inputView addSubview:otherSubInputView];
3156 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3157 [inputView setTextInputClient:123];
3158 [inputView reloadInputViews];
3159 [subInputView setTextInputClient:123];
3160 [subInputView reloadInputViews];
3161 [otherSubInputView setTextInputClient:123];
3162 [otherSubInputView reloadInputViews];
3163 [subFirstResponderInputView setTextInputClient:123];
3164 [subFirstResponderInputView reloadInputViews];
3165 [subFirstResponderInputView becomeFirstResponder];
3167 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3168 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3172 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3174 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3175 [inputView setTextInputClient:123];
3176 [inputView reloadInputViews];
3178 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3179 XCTAssertNil(firstResponder);
3183 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3184 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3185 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3186 UIScene* scene = scenes.anyObject;
3187 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3188 UIWindowScene* windowScene = (UIWindowScene*)scene;
3189 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3190 UIWindow* window = windowScene.windows[0];
3191 [window addSubview:viewController.view];
3193 [viewController loadView];
3195 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3196 initWithDescription:
3197 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3198 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3199 .andDo(^(NSInvocation* invocation) {
3200 [expectation fulfill];
3202 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3203 [NSNotificationCenter.defaultCenter
3204 postNotificationName:UIKeyboardWillShowNotification
3206 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3210 [textInputPlugin handleMethodCall:initialMoveCall
3211 result:^(id _Nullable result){
3216 [textInputPlugin handleMethodCall:subsequentMoveCall
3217 result:^(id _Nullable result){
3223 [textInputPlugin handleMethodCall:pointerUpCall
3224 result:^(id _Nullable result){
3227 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3231 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3232 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3233 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3234 UIScene* scene = scenes.anyObject;
3235 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3236 UIWindowScene* windowScene = (UIWindowScene*)scene;
3237 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3238 UIWindow* window = windowScene.windows[0];
3239 [window addSubview:viewController.view];
3241 [viewController loadView];
3243 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3244 [NSNotificationCenter.defaultCenter
3245 postNotificationName:UIKeyboardWillShowNotification
3247 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3251 [textInputPlugin handleMethodCall:initialMoveCall
3252 result:^(id _Nullable result){
3257 [textInputPlugin handleMethodCall:subsequentMoveCall
3258 result:^(id _Nullable result){
3264 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3265 result:^(id _Nullable result){
3271 [textInputPlugin handleMethodCall:pointerUpCall
3272 result:^(id _Nullable result){
3274 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3275 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3277 XCTNSPredicateExpectation* expectation =
3278 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3279 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3283 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3284 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3285 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3286 UIScene* scene = scenes.anyObject;
3287 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3288 UIWindowScene* windowScene = (UIWindowScene*)scene;
3289 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3290 UIWindow* window = windowScene.windows[0];
3291 [window addSubview:viewController.view];
3293 [viewController loadView];
3296 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3298 [inputView setTextInputClient:123];
3299 [inputView reloadInputViews];
3300 [inputView becomeFirstResponder];
3302 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3303 [NSNotificationCenter.defaultCenter
3304 postNotificationName:UIKeyboardWillShowNotification
3306 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3310 [textInputPlugin handleMethodCall:initialMoveCall
3311 result:^(id _Nullable result){
3316 [textInputPlugin handleMethodCall:subsequentMoveCall
3317 result:^(id _Nullable result){
3323 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3324 result:^(id _Nullable result){
3330 [textInputPlugin handleMethodCall:pointerUpCall
3331 result:^(id _Nullable result){
3333 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3334 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3336 XCTNSPredicateExpectation* expectation =
3337 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3338 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3342 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3343 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3344 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3345 UIScene* scene = scenes.anyObject;
3346 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3347 UIWindowScene* windowScene = (UIWindowScene*)scene;
3348 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3349 UIWindow* window = windowScene.windows[0];
3350 [window addSubview:viewController.view];
3352 [viewController loadView];
3354 XCTestExpectation* expectation =
3355 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3356 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3357 [NSNotificationCenter.defaultCenter
3358 postNotificationName:UIKeyboardWillShowNotification
3360 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3364 [textInputPlugin handleMethodCall:initialMoveCall
3365 result:^(id _Nullable result){
3370 [textInputPlugin handleMethodCall:subsequentMoveCall
3371 result:^(id _Nullable result){
3376 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3377 result:^(id _Nullable result){
3384 handleMethodCall:pointerUpCall
3385 result:^(id _Nullable result) {
3386 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3387 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3388 keyboardFrame.origin.y);
3389 [expectation fulfill];
3394 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3395 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3396 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3397 UIScene* scene = scenes.anyObject;
3398 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3399 UIWindowScene* windowScene = (UIWindowScene*)scene;
3400 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3401 UIWindow* window = windowScene.windows[0];
3402 [window addSubview:viewController.view];
3404 [viewController loadView];
3406 XCTestExpectation* expectation =
3407 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3408 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3409 [NSNotificationCenter.defaultCenter
3410 postNotificationName:UIKeyboardWillShowNotification
3412 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3416 [textInputPlugin handleMethodCall:initialMoveCall
3417 result:^(id _Nullable result){
3422 [textInputPlugin handleMethodCall:subsequentMoveCall
3423 result:^(id _Nullable result){
3430 handleMethodCall:pointerUpCall
3431 result:^(id _Nullable result) {
3432 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3433 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3434 [expectation fulfill];
3438 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3439 [UIView setAnimationsEnabled:YES];
3440 [textInputPlugin showKeyboardAndRemoveScreenshot];
3442 UIView.areAnimationsEnabled,
3443 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3446 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3447 [UIView setAnimationsEnabled:YES];
3448 [textInputPlugin showKeyboardAndRemoveScreenshot];
3450 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3452 return UIView.areAnimationsEnabled;
3454 XCTNSPredicateExpectation* expectation =
3455 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3456 [
self waitForExpectations:@[ expectation ] timeout:10.0];