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)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
311 NSDictionary* config =
self.mutableTemplateCopy;
312 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
313 [
self setClientId:123 configuration:config];
318 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
319 [
self setClientId:124 configuration:config];
324 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
328 if (@available(iOS 17.0, *)) {
330 OCMVerify(never(), [
engine flutterTextInputView:inputView
331 showAutocorrectionPromptRectForStart:0
335 OCMVerify([
engine flutterTextInputView:inputView
336 showAutocorrectionPromptRectForStart:0
342 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
344 __block
int updateCount = 0;
345 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
346 .andDo(^(NSInvocation* invocation) {
350 [inputView.text setString:@"Some initial text"];
351 XCTAssertEqual(updateCount, 0);
354 [inputView setSelectedTextRange:textRange];
355 XCTAssertEqual(updateCount, 1);
358 NSDictionary* config =
self.mutableTemplateCopy;
359 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
360 [config setValue:@(NO) forKey:@"obscureText"];
361 [config setValue:@(NO) forKey:@"enableDeltaModel"];
362 [inputView configureWithDictionary:config];
365 [inputView setSelectedTextRange:textRange];
367 XCTAssertEqual(updateCount, 1);
370 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
372 if (@available(iOS 17.0, *)) {
376 if (@available(iOS 14.0, *)) {
379 __block
int callCount = 0;
380 OCMStub([
engine flutterTextInputView:inputView
381 showAutocorrectionPromptRectForStart:0
384 .andDo(^(NSInvocation* invocation) {
390 XCTAssertEqual(callCount, 1);
392 UIScribbleInteraction* scribbleInteraction =
393 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
395 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
399 XCTAssertEqual(callCount, 1);
401 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
402 [inputView resetScribbleInteractionStatusIfEnding];
405 XCTAssertEqual(callCount, 2);
407 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
411 XCTAssertEqual(callCount, 2);
413 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
417 XCTAssertEqual(callCount, 2);
419 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
422 XCTAssertEqual(callCount, 3);
426 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
432 arguments:@[ @(123),
self.mutableTemplateCopy ]];
434 result:^(id _Nullable result){
441 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
445 arguments:@{@"transform" : yOffsetMatrix}];
447 result:^(id _Nullable result){
450 if (@available(iOS 17, *)) {
451 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
452 @"The input hider should overlap with the text on and after iOS 17");
455 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
456 @"The input hider should be on the origin of screen on and before iOS 16.");
460 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
466 toPosition:toPosition];
467 NSRange range = flutterRange.
range;
469 XCTAssertEqual(range.location, 0ul);
470 XCTAssertEqual(range.length, 2ul);
473 - (void)testTextInRange {
474 NSDictionary* config =
self.mutableTemplateCopy;
475 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
476 [
self setClientId:123 configuration:config];
477 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
480 [inputView insertText:@"test"];
483 NSString* substring = [inputView textInRange:range];
484 XCTAssertEqual(substring.length, 4ul);
487 substring = [inputView textInRange:range];
488 XCTAssertEqual(substring.length, 0ul);
491 - (void)testStandardEditActions {
492 NSDictionary* config =
self.mutableTemplateCopy;
493 [
self setClientId:123 configuration:config];
494 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
497 [inputView insertText:@"aaaa"];
498 [inputView selectAll:nil];
500 [inputView insertText:@"bbbb"];
501 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
502 [inputView paste:nil];
503 [inputView selectAll:nil];
504 [inputView copy:nil];
505 [inputView paste:nil];
506 [inputView selectAll:nil];
507 [inputView delete:nil];
508 [inputView paste:nil];
509 [inputView paste:nil];
512 NSString* substring = [inputView textInRange:range];
513 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
516 - (void)testDeletingBackward {
517 NSDictionary* config =
self.mutableTemplateCopy;
518 [
self setClientId:123 configuration:config];
519 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
522 [inputView insertText:@"á ž¹ð Ÿ˜€ text 𠟥°ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦ð Ÿ‡ºð Ÿ‡³à ¸”à ¸µ "];
523 [inputView deleteBackward];
524 [inputView deleteBackward];
527 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³à¸”");
528 [inputView deleteBackward];
529 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³");
530 [inputView deleteBackward];
531 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦");
532 [inputView deleteBackward];
533 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
534 [inputView deleteBackward];
536 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
537 [inputView deleteBackward];
538 [inputView deleteBackward];
539 [inputView deleteBackward];
540 [inputView deleteBackward];
541 [inputView deleteBackward];
542 [inputView deleteBackward];
544 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
545 [inputView deleteBackward];
546 XCTAssertEqualObjects(inputView.text,
@"áž¹");
547 [inputView deleteBackward];
548 XCTAssertEqualObjects(inputView.text,
@"");
553 - (void)testSystemOnlyAddingPartialComposedCharacter {
554 NSDictionary* config =
self.mutableTemplateCopy;
555 [
self setClientId:123 configuration:config];
556 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
559 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
560 [inputView deleteBackward];
563 [inputView insertText:[@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)]];
564 [inputView insertText:@"ì •„"];
566 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ì•„");
569 [inputView deleteBackward];
572 [inputView insertText:@"𠟘€"];
573 [inputView deleteBackward];
575 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
576 [inputView insertText:@"ì •„"];
577 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
580 [inputView deleteBackward];
583 [inputView deleteBackward];
585 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
586 [inputView insertText:@"ì •„"];
588 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
591 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
592 NSDictionary* config =
self.mutableTemplateCopy;
593 [
self setClientId:123 configuration:config];
594 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
597 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
598 [inputView deleteBackward];
599 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
602 NSString* brokenEmoji = [@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)];
603 [inputView insertText:brokenEmoji];
604 [inputView insertText:@"ì •„"];
606 NSString* finalText = [NSString stringWithFormat:@"%@ì •„", brokenEmoji];
607 XCTAssertEqualObjects(inputView.text, finalText);
610 - (void)testPastingNonTextDisallowed {
611 NSDictionary* config =
self.mutableTemplateCopy;
612 [
self setClientId:123 configuration:config];
613 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
616 UIPasteboard.generalPasteboard.color = UIColor.redColor;
617 XCTAssertNil(UIPasteboard.generalPasteboard.string);
618 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
619 [inputView paste:nil];
621 XCTAssertEqualObjects(inputView.text,
@"");
624 - (void)testNoZombies {
631 [passwordView.textField description];
633 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
636 - (void)testInputViewCrash {
641 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
642 activeView = inputPlugin.activeView;
644 [activeView updateEditingState];
647 - (void)testDoNotReuseInputViews {
648 NSDictionary* config =
self.mutableTemplateCopy;
649 [
self setClientId:123 configuration:config];
651 [
self setClientId:456 configuration:config];
653 XCTAssertNotNil(currentView);
658 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
660 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
664 - (void)testPropagatePressEventsToViewController {
666 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
667 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
671 NSDictionary* config =
self.mutableTemplateCopy;
672 [
self setClientId:123 configuration:config];
674 [
self setTextInputShow];
676 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
677 withEvent:OCMClassMock([UIPressesEvent class])];
679 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
680 withEvent:[OCMArg isNotNil]]);
681 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
682 withEvent:[OCMArg isNotNil]]);
684 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
685 withEvent:OCMClassMock([UIPressesEvent class])];
687 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
688 withEvent:[OCMArg isNotNil]]);
689 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
690 withEvent:[OCMArg isNotNil]]);
693 - (void)testPropagatePressEventsToViewController2 {
695 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
696 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
700 NSDictionary* config =
self.mutableTemplateCopy;
701 [
self setClientId:123 configuration:config];
702 [
self setTextInputShow];
705 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
706 withEvent:OCMClassMock([UIPressesEvent class])];
708 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
709 withEvent:[OCMArg isNotNil]]);
710 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
711 withEvent:[OCMArg isNotNil]]);
714 [
self setClientId:321 configuration:config];
715 [
self setTextInputShow];
717 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
719 [currentView pressesEnded:[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(1), [mockViewController pressesEnded:[OCMArg isNotNil]
725 withEvent:[OCMArg isNotNil]]);
728 - (void)testUpdateSecureTextEntry {
729 NSDictionary* config =
self.mutableTemplateCopy;
730 [config setValue:@"YES" forKey:@"obscureText"];
731 [
self setClientId:123 configuration:config];
733 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
736 __block
int callCount = 0;
737 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
741 XCTAssertTrue(inputView.isSecureTextEntry);
743 config =
self.mutableTemplateCopy;
744 [config setValue:@"NO" forKey:@"obscureText"];
745 [
self updateConfig:config];
747 XCTAssertEqual(callCount, 1);
748 XCTAssertFalse(inputView.isSecureTextEntry);
751 - (void)testInputActionContinueAction {
767 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
769 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
772 - (void)testDisablingAutocorrectDisablesSpellChecking {
776 NSDictionary* config =
self.mutableTemplateCopy;
777 [inputView configureWithDictionary:config];
779 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
780 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
782 [config setValue:@(NO) forKey:@"autocorrect"];
783 [inputView configureWithDictionary:config];
785 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
786 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
789 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
791 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
805 XCTAssertEqual(inputView.markedTextRange, nil);
808 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
810 BOOL respondsToInsertionPointColor =
811 [inputView respondsToSelector:@selector(insertionPointColor)];
812 if (@available(iOS 17, *)) {
813 XCTAssertFalse(respondsToInsertionPointColor);
815 XCTAssertTrue(respondsToInsertionPointColor);
819 #pragma mark - TextEditingDelta tests
820 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
822 inputView.enableDeltaModel = YES;
824 __block
int updateCount = 0;
826 [inputView insertText:@"text to insert"];
829 flutterTextInputView:inputView
830 updateEditingClient:0
831 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
832 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
833 isEqualToString:
@""]) &&
834 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
835 isEqualToString:
@"text to insert"]) &&
836 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
837 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
839 .andDo(^(NSInvocation* invocation) {
842 XCTAssertEqual(updateCount, 0);
844 [
self flushScheduledAsyncBlocks];
847 XCTAssertEqual(updateCount, 1);
849 [inputView deleteBackward];
850 OCMExpect([
engine flutterTextInputView:inputView
851 updateEditingClient:0
852 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
853 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
854 isEqualToString:
@"text to insert"]) &&
855 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
856 isEqualToString:
@""]) &&
857 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
859 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
862 .andDo(^(NSInvocation* invocation) {
865 [
self flushScheduledAsyncBlocks];
866 XCTAssertEqual(updateCount, 2);
869 OCMExpect([
engine flutterTextInputView:inputView
870 updateEditingClient:0
871 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
872 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
873 isEqualToString:
@"text to inser"]) &&
874 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
875 isEqualToString:
@""]) &&
876 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
878 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
881 .andDo(^(NSInvocation* invocation) {
884 [
self flushScheduledAsyncBlocks];
885 XCTAssertEqual(updateCount, 3);
888 withText:@"replace text"];
891 flutterTextInputView:inputView
892 updateEditingClient:0
893 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
894 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
895 isEqualToString:
@"text to inser"]) &&
896 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
897 isEqualToString:
@"replace text"]) &&
898 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
899 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
901 .andDo(^(NSInvocation* invocation) {
904 [
self flushScheduledAsyncBlocks];
905 XCTAssertEqual(updateCount, 4);
907 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
908 OCMExpect([
engine flutterTextInputView:inputView
909 updateEditingClient:0
910 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
911 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
912 isEqualToString:
@"replace textext to inser"]) &&
913 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
914 isEqualToString:
@"marked text"]) &&
915 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
917 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
920 .andDo(^(NSInvocation* invocation) {
923 [
self flushScheduledAsyncBlocks];
924 XCTAssertEqual(updateCount, 5);
926 [inputView unmarkText];
928 flutterTextInputView:inputView
929 updateEditingClient:0
930 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
931 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
932 isEqualToString:
@"replace textmarked textext to inser"]) &&
933 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
934 isEqualToString:
@""]) &&
935 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
937 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
940 .andDo(^(NSInvocation* invocation) {
943 [
self flushScheduledAsyncBlocks];
945 XCTAssertEqual(updateCount, 6);
949 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
952 inputView.enableDeltaModel = YES;
955 OCMExpect([
engine flutterTextInputView:inputView
956 updateEditingClient:0
957 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
958 NSArray* deltas = state[@"deltas"];
959 NSDictionary* firstDelta = deltas[0];
960 NSDictionary* secondDelta = deltas[1];
961 NSDictionary* thirdDelta = deltas[2];
962 return [firstDelta[@"oldText"] isEqualToString:@""] &&
963 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
964 [firstDelta[@"deltaStart"] intValue] == 0 &&
965 [firstDelta[@"deltaEnd"] intValue] == 0 &&
966 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
967 [secondDelta[@"deltaText"] isEqualToString:@""] &&
968 [secondDelta[@"deltaStart"] intValue] == 0 &&
969 [secondDelta[@"deltaEnd"] intValue] == 1 &&
970 [thirdDelta[@"oldText"] isEqualToString:@""] &&
971 [thirdDelta[@"deltaText"] isEqualToString:@"â €”"] &&
972 [thirdDelta[@"deltaStart"] intValue] == 0 &&
973 [thirdDelta[@"deltaEnd"] intValue] == 0;
977 [inputView insertText:@"-"];
978 [inputView deleteBackward];
979 [inputView insertText:@"â €”"];
981 [
self flushScheduledAsyncBlocks];
985 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
987 inputView.enableDeltaModel = YES;
989 __block
int updateCount = 0;
990 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
991 .andDo(^(NSInvocation* invocation) {
995 [inputView.text setString:@"Some initial text"];
996 XCTAssertEqual(updateCount, 0);
999 inputView.markedTextRange = range;
1000 inputView.selectedTextRange = nil;
1001 [
self flushScheduledAsyncBlocks];
1002 XCTAssertEqual(updateCount, 1);
1004 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1006 flutterTextInputView:inputView
1007 updateEditingClient:0
1008 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1009 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1010 isEqualToString:
@"Some initial text"]) &&
1011 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1012 isEqualToString:
@"new marked text."]) &&
1013 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1014 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1016 [
self flushScheduledAsyncBlocks];
1017 XCTAssertEqual(updateCount, 2);
1020 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1022 inputView.enableDeltaModel = YES;
1024 __block
int updateCount = 0;
1025 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1026 .andDo(^(NSInvocation* invocation) {
1030 [inputView.text setString:@"Some initial text"];
1031 [
self flushScheduledAsyncBlocks];
1032 XCTAssertEqual(updateCount, 0);
1035 inputView.markedTextRange = range;
1036 inputView.selectedTextRange = nil;
1037 [
self flushScheduledAsyncBlocks];
1038 XCTAssertEqual(updateCount, 1);
1040 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1042 flutterTextInputView:inputView
1043 updateEditingClient:0
1044 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1045 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1046 isEqualToString:
@"Some initial text"]) &&
1047 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1048 isEqualToString:
@"text."]) &&
1049 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1050 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1052 [
self flushScheduledAsyncBlocks];
1053 XCTAssertEqual(updateCount, 2);
1056 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1058 inputView.enableDeltaModel = YES;
1060 __block
int updateCount = 0;
1061 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1062 .andDo(^(NSInvocation* invocation) {
1066 [inputView.text setString:@"Some initial text"];
1067 [
self flushScheduledAsyncBlocks];
1068 XCTAssertEqual(updateCount, 0);
1071 inputView.markedTextRange = range;
1072 inputView.selectedTextRange = nil;
1073 [
self flushScheduledAsyncBlocks];
1074 XCTAssertEqual(updateCount, 1);
1076 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1078 flutterTextInputView:inputView
1079 updateEditingClient:0
1080 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1081 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1082 isEqualToString:
@"Some initial text"]) &&
1083 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1084 isEqualToString:
@"tex"]) &&
1085 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1086 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1088 [
self flushScheduledAsyncBlocks];
1089 XCTAssertEqual(updateCount, 2);
1092 #pragma mark - EditingState tests
1094 - (void)testUITextInputCallsUpdateEditingStateOnce {
1097 __block
int updateCount = 0;
1098 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1099 .andDo(^(NSInvocation* invocation) {
1103 [inputView insertText:@"text to insert"];
1105 XCTAssertEqual(updateCount, 1);
1107 [inputView deleteBackward];
1108 XCTAssertEqual(updateCount, 2);
1111 XCTAssertEqual(updateCount, 3);
1114 withText:@"replace text"];
1115 XCTAssertEqual(updateCount, 4);
1117 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1118 XCTAssertEqual(updateCount, 5);
1120 [inputView unmarkText];
1121 XCTAssertEqual(updateCount, 6);
1124 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1126 inputView.enableDeltaModel = YES;
1128 __block
int updateCount = 0;
1129 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1130 .andDo(^(NSInvocation* invocation) {
1134 [inputView insertText:@"text to insert"];
1135 [
self flushScheduledAsyncBlocks];
1137 XCTAssertEqual(updateCount, 1);
1139 [inputView deleteBackward];
1140 [
self flushScheduledAsyncBlocks];
1141 XCTAssertEqual(updateCount, 2);
1144 [
self flushScheduledAsyncBlocks];
1145 XCTAssertEqual(updateCount, 3);
1148 withText:@"replace text"];
1149 [
self flushScheduledAsyncBlocks];
1150 XCTAssertEqual(updateCount, 4);
1152 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1153 [
self flushScheduledAsyncBlocks];
1154 XCTAssertEqual(updateCount, 5);
1156 [inputView unmarkText];
1157 [
self flushScheduledAsyncBlocks];
1158 XCTAssertEqual(updateCount, 6);
1161 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1164 __block
int updateCount = 0;
1165 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1166 .andDo(^(NSInvocation* invocation) {
1170 [inputView.text setString:@"BEFORE"];
1171 XCTAssertEqual(updateCount, 0);
1173 inputView.markedTextRange = nil;
1174 inputView.selectedTextRange = nil;
1175 XCTAssertEqual(updateCount, 1);
1178 XCTAssertEqual(updateCount, 1);
1179 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1180 XCTAssertEqual(updateCount, 1);
1181 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1182 XCTAssertEqual(updateCount, 1);
1186 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1187 XCTAssertEqual(updateCount, 1);
1189 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1190 XCTAssertEqual(updateCount, 1);
1194 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1195 XCTAssertEqual(updateCount, 1);
1197 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1198 XCTAssertEqual(updateCount, 1);
1201 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1203 inputView.enableDeltaModel = YES;
1205 __block
int updateCount = 0;
1206 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1207 .andDo(^(NSInvocation* invocation) {
1211 [inputView.text setString:@"BEFORE"];
1212 [
self flushScheduledAsyncBlocks];
1213 XCTAssertEqual(updateCount, 0);
1215 inputView.markedTextRange = nil;
1216 inputView.selectedTextRange = nil;
1217 [
self flushScheduledAsyncBlocks];
1218 XCTAssertEqual(updateCount, 1);
1221 XCTAssertEqual(updateCount, 1);
1222 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1223 [
self flushScheduledAsyncBlocks];
1224 XCTAssertEqual(updateCount, 1);
1226 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1227 [
self flushScheduledAsyncBlocks];
1228 XCTAssertEqual(updateCount, 1);
1232 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1233 [
self flushScheduledAsyncBlocks];
1234 XCTAssertEqual(updateCount, 1);
1237 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1238 [
self flushScheduledAsyncBlocks];
1239 XCTAssertEqual(updateCount, 1);
1243 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1244 [
self flushScheduledAsyncBlocks];
1245 XCTAssertEqual(updateCount, 1);
1248 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1249 [
self flushScheduledAsyncBlocks];
1250 XCTAssertEqual(updateCount, 1);
1253 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1256 __block
int updateCount = 0;
1257 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1258 .andDo(^(NSInvocation* invocation) {
1262 [inputView unmarkText];
1264 XCTAssertEqual(updateCount, 0);
1266 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1268 XCTAssertEqual(updateCount, 1);
1270 [inputView unmarkText];
1272 XCTAssertEqual(updateCount, 2);
1275 - (void)testCanCopyPasteWithScribbleEnabled {
1276 if (@available(iOS 14.0, *)) {
1277 NSDictionary* config =
self.mutableTemplateCopy;
1278 [
self setClientId:123 configuration:config];
1279 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1285 [mockInputView insertText:@"aaaa"];
1286 [mockInputView selectAll:nil];
1288 XCTAssertFalse([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1289 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1290 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1291 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1293 [mockInputView copy:NULL];
1294 XCTAssertFalse([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1295 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1296 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1297 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1301 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1302 if (@available(iOS 14.0, *)) {
1305 __block
int updateCount = 0;
1306 OCMStub([
engine flutterTextInputView:inputView
1307 updateEditingClient:0
1308 withState:[OCMArg isNotNil]])
1309 .andDo(^(NSInvocation* invocation) {
1313 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1315 XCTAssertEqual(updateCount, 1);
1317 UIScribbleInteraction* scribbleInteraction =
1318 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1320 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1321 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1323 XCTAssertEqual(updateCount, 1);
1325 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1326 [inputView resetScribbleInteractionStatusIfEnding];
1327 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1329 XCTAssertEqual(updateCount, 2);
1331 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1332 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1335 XCTAssertEqual(updateCount, 2);
1337 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1338 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1341 XCTAssertEqual(updateCount, 2);
1343 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1344 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1346 XCTAssertEqual(updateCount, 3);
1350 - (void)testUpdateEditingClientNegativeSelection {
1353 [inputView.text setString:@"SELECTION"];
1354 inputView.markedTextRange = nil;
1355 inputView.selectedTextRange = nil;
1357 [inputView setTextInputState:@{
1358 @"text" : @"SELECTION",
1359 @"selectionBase" : @-1,
1360 @"selectionExtent" : @-1
1362 [inputView updateEditingState];
1363 OCMVerify([
engine flutterTextInputView:inputView
1364 updateEditingClient:0
1365 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1366 return ([state[
@"selectionBase"] intValue]) == 0 &&
1367 ([state[
@"selectionExtent"] intValue] == 0);
1372 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1373 [inputView updateEditingState];
1374 OCMVerify([
engine flutterTextInputView:inputView
1375 updateEditingClient:0
1376 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1377 return ([state[
@"selectionBase"] intValue]) == 0 &&
1378 ([state[
@"selectionExtent"] intValue] == 0);
1382 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1383 [inputView updateEditingState];
1384 OCMVerify([
engine flutterTextInputView:inputView
1385 updateEditingClient:0
1386 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1387 return ([state[
@"selectionBase"] intValue]) == 0 &&
1388 ([state[
@"selectionExtent"] intValue] == 0);
1392 - (void)testUpdateEditingClientSelectionClamping {
1396 [inputView.text setString:@"SELECTION"];
1397 inputView.markedTextRange = nil;
1398 inputView.selectedTextRange = nil;
1401 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1402 [inputView updateEditingState];
1403 OCMVerify([
engine flutterTextInputView:inputView
1404 updateEditingClient:0
1405 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1406 return ([state[
@"selectionBase"] intValue]) == 0 &&
1407 ([state[
@"selectionExtent"] intValue] == 0);
1411 [inputView setTextInputState:@{
1412 @"text" : @"SELECTION",
1413 @"selectionBase" : @0,
1414 @"selectionExtent" : @9999
1416 [inputView updateEditingState];
1418 OCMVerify([
engine flutterTextInputView:inputView
1419 updateEditingClient:0
1420 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1421 return ([state[
@"selectionBase"] intValue]) == 0 &&
1422 ([state[
@"selectionExtent"] intValue] == 9);
1427 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1428 [inputView updateEditingState];
1429 OCMVerify([
engine flutterTextInputView:inputView
1430 updateEditingClient:0
1431 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1432 return ([state[
@"selectionBase"] intValue]) == 0 &&
1433 ([state[
@"selectionExtent"] intValue] == 1);
1437 [inputView setTextInputState:@{
1438 @"text" : @"SELECTION",
1439 @"selectionBase" : @9999,
1440 @"selectionExtent" : @9999
1442 [inputView updateEditingState];
1443 OCMVerify([
engine flutterTextInputView:inputView
1444 updateEditingClient:0
1445 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1446 return ([state[
@"selectionBase"] intValue]) == 9 &&
1447 ([state[
@"selectionExtent"] intValue] == 9);
1451 - (void)testInputViewsHasNonNilInputDelegate {
1452 if (@available(iOS 13.0, *)) {
1454 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1456 [inputView setTextInputClient:123];
1457 [inputView reloadInputViews];
1458 [inputView becomeFirstResponder];
1459 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1460 inputView.inputDelegate = nil;
1463 [mockInputView setTextInputState:@{
1464 @"text" : @"COMPOSING",
1465 @"composingBase" : @1,
1466 @"composingExtent" : @3
1468 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1469 [inputView removeFromSuperview];
1473 - (void)testInputViewsDoNotHaveUITextInteractions {
1474 if (@available(iOS 13.0, *)) {
1476 BOOL hasTextInteraction = NO;
1477 for (
id interaction in inputView.interactions) {
1478 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1479 if (hasTextInteraction) {
1483 XCTAssertFalse(hasTextInteraction);
1487 #pragma mark - UITextInput methods - Tests
1489 - (void)testUpdateFirstRectForRange {
1490 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1496 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1501 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1502 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1506 NSArray* affineMatrix = @[
1507 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1508 @(-6.0), @(3.0), @(9.0), @(1.0)
1512 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1514 [inputView setEditableTransform:yOffsetMatrix];
1516 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1519 CGRect testRect = CGRectMake(0, 0, 100, 100);
1520 [inputView setMarkedRect:testRect];
1522 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1523 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1525 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1528 [inputView setEditableTransform:zeroMatrix];
1530 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1531 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1534 [inputView setEditableTransform:yOffsetMatrix];
1535 [inputView setMarkedRect:testRect];
1536 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1539 [inputView setMarkedRect:kInvalidFirstRect];
1541 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1542 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1545 [inputView setEditableTransform:affineMatrix];
1546 [inputView setMarkedRect:testRect];
1548 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1550 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1551 const CGPoint offset = CGPointMake(113, 119);
1552 CGRect currentFrame = inputView.frame;
1553 currentFrame.origin = offset;
1554 inputView.frame = currentFrame;
1557 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1558 [inputView firstRectForRange:range]));
1561 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1563 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1568 [inputView setSelectionRects:@[
1577 if (@available(iOS 17, *)) {
1578 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1579 [inputView firstRectForRange:multiRectRange]));
1581 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1582 [inputView firstRectForRange:multiRectRange]));
1586 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1588 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1590 [inputView setSelectionRects:@[
1597 if (@available(iOS 17, *)) {
1598 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1599 [inputView firstRectForRange:singleRectRange]));
1601 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1606 if (@available(iOS 17, *)) {
1607 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1608 [inputView firstRectForRange:multiRectRange]));
1610 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1613 [inputView setTextInputState:@{@"text" : @"COM"}];
1615 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1618 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1620 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1622 [inputView setSelectionRects:@[
1629 if (@available(iOS 17, *)) {
1630 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1631 [inputView firstRectForRange:singleRectRange]));
1633 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1637 if (@available(iOS 17, *)) {
1638 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1639 [inputView firstRectForRange:multiRectRange]));
1641 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1644 [inputView setTextInputState:@{@"text" : @"COM"}];
1646 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1649 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1651 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1653 [inputView setSelectionRects:@[
1664 if (@available(iOS 17, *)) {
1665 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1666 [inputView firstRectForRange:singleRectRange]));
1668 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1673 if (@available(iOS 17, *)) {
1674 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1675 [inputView firstRectForRange:multiRectRange]));
1677 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1681 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1683 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1685 [inputView setSelectionRects:@[
1696 if (@available(iOS 17, *)) {
1697 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1698 [inputView firstRectForRange:singleRectRange]));
1700 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1704 if (@available(iOS 17, *)) {
1705 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1706 [inputView firstRectForRange:multiRectRange]));
1708 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1712 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1714 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1716 [inputView setSelectionRects:@[
1727 if (@available(iOS 17, *)) {
1728 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1729 [inputView firstRectForRange:multiRectRange]));
1731 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1735 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1737 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1739 [inputView setSelectionRects:@[
1750 if (@available(iOS 17, *)) {
1751 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1752 [inputView firstRectForRange:multiRectRange]));
1754 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1758 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1760 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1762 [inputView setSelectionRects:@[
1773 if (@available(iOS 17, *)) {
1774 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1775 [inputView firstRectForRange:multiRectRange]));
1777 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1781 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1783 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1785 [inputView setSelectionRects:@[
1796 if (@available(iOS 17, *)) {
1797 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1798 [inputView firstRectForRange:multiRectRange]));
1800 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1804 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1806 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1808 [inputView setSelectionRects:@[
1819 if (@available(iOS 17, *)) {
1820 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1821 [inputView firstRectForRange:multiRectRange]));
1823 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1827 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1829 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1831 [inputView setSelectionRects:@[
1842 if (@available(iOS 17, *)) {
1843 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1844 [inputView firstRectForRange:multiRectRange]));
1846 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1850 - (void)testClosestPositionToPoint {
1852 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1855 [inputView setSelectionRects:@[
1860 CGPoint point = CGPointMake(150, 150);
1861 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1862 XCTAssertEqual(UITextStorageDirectionBackward,
1867 [inputView setSelectionRects:@[
1874 point = CGPointMake(125, 150);
1875 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1876 XCTAssertEqual(UITextStorageDirectionForward,
1881 [inputView setSelectionRects:@[
1888 point = CGPointMake(125, 201);
1889 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1890 XCTAssertEqual(UITextStorageDirectionBackward,
1894 [inputView setSelectionRects:@[
1900 point = CGPointMake(125, 250);
1901 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1902 XCTAssertEqual(UITextStorageDirectionBackward,
1906 [inputView setSelectionRects:@[
1911 point = CGPointMake(110, 50);
1912 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1913 XCTAssertEqual(UITextStorageDirectionForward,
1918 [inputView beginFloatingCursorAtPoint:CGPointZero];
1919 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1920 XCTAssertEqual(UITextStorageDirectionForward,
1922 [inputView endFloatingCursor];
1925 - (void)testClosestPositionToPointRTL {
1927 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1929 [inputView setSelectionRects:@[
1945 XCTAssertEqual(0U, position.
index);
1946 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1948 XCTAssertEqual(1U, position.
index);
1949 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1951 XCTAssertEqual(1U, position.
index);
1952 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1954 XCTAssertEqual(2U, position.
index);
1955 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1957 XCTAssertEqual(2U, position.
index);
1958 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1960 XCTAssertEqual(3U, position.
index);
1961 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1963 XCTAssertEqual(3U, position.
index);
1964 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1967 - (void)testSelectionRectsForRange {
1969 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1971 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
1972 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
1973 [inputView setSelectionRects:@[
1982 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
1983 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
1984 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
1988 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
1989 XCTAssertTrue(CGRectEqualToRect(
1990 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
1991 [inputView selectionRectsForRange:range][0].rect));
1994 - (void)testClosestPositionToPointWithinRange {
1996 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1999 [inputView setSelectionRects:@[
2006 CGPoint point = CGPointMake(125, 150);
2009 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2011 UITextStorageDirectionForward,
2012 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2015 [inputView setSelectionRects:@[
2022 point = CGPointMake(125, 150);
2025 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2027 UITextStorageDirectionForward,
2028 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2031 - (void)testClosestPositionToPointWithPartialSelectionRects {
2033 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2040 XCTAssertTrue(CGRectEqualToRect(
2043 affinity:UITextStorageDirectionForward]],
2044 CGRectMake(100, 0, 0, 100)));
2047 XCTAssertTrue(CGRectEqualToRect(
2050 affinity:UITextStorageDirectionForward]],
2054 #pragma mark - Floating Cursor - Tests
2056 - (void)testFloatingCursorDoesNotThrow {
2059 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2060 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2061 [inputView endFloatingCursor];
2062 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2063 [inputView endFloatingCursor];
2066 - (void)testFloatingCursor {
2068 [inputView setTextInputState:@{
2070 @"selectionBase" : @1,
2071 @"selectionExtent" : @1,
2082 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2085 XCTAssertTrue(CGRectEqualToRect(
2088 affinity:UITextStorageDirectionForward]],
2089 CGRectMake(0, 0, 0, 100)));
2092 XCTAssertTrue(CGRectEqualToRect(
2095 affinity:UITextStorageDirectionForward]],
2096 CGRectMake(100, 100, 0, 100)));
2097 XCTAssertTrue(CGRectEqualToRect(
2100 affinity:UITextStorageDirectionForward]],
2101 CGRectMake(200, 200, 0, 100)));
2102 XCTAssertTrue(CGRectEqualToRect(
2105 affinity:UITextStorageDirectionForward]],
2106 CGRectMake(300, 300, 0, 100)));
2109 XCTAssertTrue(CGRectEqualToRect(
2112 affinity:UITextStorageDirectionForward]],
2113 CGRectMake(400, 300, 0, 100)));
2115 XCTAssertTrue(CGRectEqualToRect(
2118 affinity:UITextStorageDirectionForward]],
2122 [inputView setTextInputState:@{
2124 @"selectionBase" : @2,
2125 @"selectionExtent" : @2,
2128 XCTAssertTrue(CGRectEqualToRect(
2131 affinity:UITextStorageDirectionBackward]],
2132 CGRectMake(0, 0, 0, 100)));
2135 XCTAssertTrue(CGRectEqualToRect(
2138 affinity:UITextStorageDirectionBackward]],
2139 CGRectMake(100, 0, 0, 100)));
2140 XCTAssertTrue(CGRectEqualToRect(
2143 affinity:UITextStorageDirectionBackward]],
2144 CGRectMake(200, 100, 0, 100)));
2145 XCTAssertTrue(CGRectEqualToRect(
2148 affinity:UITextStorageDirectionBackward]],
2149 CGRectMake(300, 200, 0, 100)));
2150 XCTAssertTrue(CGRectEqualToRect(
2153 affinity:UITextStorageDirectionBackward]],
2154 CGRectMake(400, 300, 0, 100)));
2156 XCTAssertTrue(CGRectEqualToRect(
2159 affinity:UITextStorageDirectionBackward]],
2164 CGRect initialBounds = inputView.bounds;
2165 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2166 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2167 OCMVerify([
engine flutterTextInputView:inputView
2168 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2170 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2171 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2172 ([state[
@"Y"] isEqualToNumber:@(0)]);
2175 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2176 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2177 OCMVerify([
engine flutterTextInputView:inputView
2178 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2180 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2181 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2182 ([state[
@"Y"] isEqualToNumber:@(333)]);
2185 [inputView endFloatingCursor];
2186 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2187 OCMVerify([
engine flutterTextInputView:inputView
2188 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2190 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2191 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2192 ([state[
@"Y"] isEqualToNumber:@(0)]);
2196 #pragma mark - UIKeyInput Overrides - Tests
2198 - (void)testInsertTextAddsPlaceholderSelectionRects {
2201 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2211 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2214 [inputView insertText:@"in"];
2242 #pragma mark - Autofill - Utilities
2244 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2247 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2248 @"keyboardAppearance" :
@"Brightness.light",
2249 @"obscureText" : @YES,
2250 @"inputAction" :
@"TextInputAction.unspecified",
2251 @"smartDashesType" :
@"0",
2252 @"smartQuotesType" :
@"0",
2253 @"autocorrect" : @YES
2257 return [_passwordTemplate mutableCopy];
2261 return [
self.installedInputViews
2262 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2265 - (void)commitAutofillContextAndVerify {
2269 [textInputPlugin handleMethodCall:methodCall
2270 result:^(id _Nullable result){
2273 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2278 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2282 #pragma mark - Autofill - Tests
2284 - (void)testDisablingAutofillOnInputClient {
2285 NSDictionary* config =
self.mutableTemplateCopy;
2286 [config setValue:@"YES" forKey:@"obscureText"];
2288 [
self setClientId:123 configuration:config];
2291 XCTAssertEqualObjects(inputView.textContentType,
@"");
2294 - (void)testAutofillEnabledByDefault {
2295 NSDictionary* config =
self.mutableTemplateCopy;
2296 [config setValue:@"NO" forKey:@"obscureText"];
2297 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2298 forKey:@"autofill"];
2300 [
self setClientId:123 configuration:config];
2303 XCTAssertNil(inputView.textContentType);
2306 - (void)testAutofillContext {
2307 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2310 @"uniqueIdentifier" : @"field1",
2311 @"hints" : @[ @"hint1" ],
2312 @"editingValue" : @{@"text" : @""}
2314 forKey:@"autofill"];
2316 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2318 @"uniqueIdentifier" : @"field2",
2319 @"hints" : @[ @"hint2" ],
2320 @"editingValue" : @{@"text" : @""}
2322 forKey:@"autofill"];
2324 NSMutableDictionary* config = [field1 mutableCopy];
2325 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2327 [
self setClientId:123 configuration:config];
2328 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2332 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2333 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2335 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2338 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2340 @"uniqueIdentifier" : @"field3",
2341 @"hints" : @[ @"hint3" ],
2342 @"editingValue" : @{@"text" : @""}
2344 forKey:@"autofill"];
2348 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2350 [
self setClientId:123 configuration:config];
2352 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2355 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2356 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2358 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2361 for (NSString* key in oldContext.allKeys) {
2362 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2366 config =
self.mutablePasswordTemplateCopy;
2369 [
self setClientId:124 configuration:config];
2370 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2372 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2375 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2376 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2379 for (NSString* key in oldContext.allKeys) {
2380 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2384 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2388 [
self setClientId:200 configuration:config];
2391 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2394 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2395 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2398 for (NSString* key in oldContext.allKeys) {
2399 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2402 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2405 - (void)testCommitAutofillContext {
2406 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2408 @"uniqueIdentifier" : @"field1",
2409 @"hints" : @[ @"hint1" ],
2410 @"editingValue" : @{@"text" : @""}
2412 forKey:@"autofill"];
2414 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2416 @"uniqueIdentifier" : @"field2",
2417 @"hints" : @[ @"hint2" ],
2418 @"editingValue" : @{@"text" : @""}
2420 forKey:@"autofill"];
2422 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2424 @"uniqueIdentifier" : @"field3",
2425 @"hints" : @[ @"hint3" ],
2426 @"editingValue" : @{@"text" : @""}
2428 forKey:@"autofill"];
2430 NSMutableDictionary* config = [field1 mutableCopy];
2431 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2433 [
self setClientId:123 configuration:config];
2434 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2436 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2438 [
self commitAutofillContextAndVerify];
2439 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2442 [
self setClientId:123 configuration:config];
2444 [
self setClientId:124 configuration:field3];
2445 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2447 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2448 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2451 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2453 [
self commitAutofillContextAndVerify];
2454 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2457 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2459 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2463 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2464 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2466 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2468 [
self commitAutofillContextAndVerify];
2469 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2472 - (void)testAutofillInputViews {
2473 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2475 @"uniqueIdentifier" : @"field1",
2476 @"hints" : @[ @"hint1" ],
2477 @"editingValue" : @{@"text" : @""}
2479 forKey:@"autofill"];
2481 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2483 @"uniqueIdentifier" : @"field2",
2484 @"hints" : @[ @"hint2" ],
2485 @"editingValue" : @{@"text" : @""}
2487 forKey:@"autofill"];
2489 NSMutableDictionary* config = [field1 mutableCopy];
2490 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2492 [
self setClientId:123 configuration:config];
2493 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2496 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2499 XCTAssertEqual(inputFields.count, 2ul);
2500 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2505 withText:@"Autofilled!"];
2506 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2509 OCMVerify([
engine flutterTextInputView:inactiveView
2510 updateEditingClient:0
2511 withState:[OCMArg isNotNil]
2512 withTag:
@"field2"]);
2515 - (void)testPasswordAutofillHack {
2516 NSDictionary* config =
self.mutableTemplateCopy;
2517 [config setValue:@"YES" forKey:@"obscureText"];
2518 [
self setClientId:123 configuration:config];
2521 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2525 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2528 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2531 - (void)testClearAutofillContextClearsSelection {
2532 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2533 NSDictionary* editingValue = @{
2534 @"text" :
@"REGULAR_TEXT_FIELD",
2535 @"composingBase" : @0,
2536 @"composingExtent" : @3,
2537 @"selectionBase" : @1,
2538 @"selectionExtent" : @4
2540 [regularField setValue:@{
2541 @"uniqueIdentifier" : @"field2",
2542 @"hints" : @[ @"hint2" ],
2543 @"editingValue" : editingValue,
2545 forKey:@"autofill"];
2546 [regularField addEntriesFromDictionary:editingValue];
2547 [
self setClientId:123 configuration:regularField];
2548 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2549 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2552 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2554 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2558 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2559 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2561 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2563 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2564 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2567 XCTAssert([oldInputView.text isEqualToString:
@""]);
2569 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2572 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2574 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2575 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2577 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2580 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2581 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2583 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2585 [
self commitAutofillContextAndVerify];
2588 - (void)testScribbleSetSelectionRects {
2589 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2590 NSDictionary* editingValue = @{
2591 @"text" :
@"REGULAR_TEXT_FIELD",
2592 @"composingBase" : @0,
2593 @"composingExtent" : @3,
2594 @"selectionBase" : @1,
2595 @"selectionExtent" : @4
2597 [regularField setValue:@{
2598 @"uniqueIdentifier" : @"field1",
2599 @"hints" : @[ @"hint2" ],
2600 @"editingValue" : editingValue,
2602 forKey:@"autofill"];
2603 [regularField addEntriesFromDictionary:editingValue];
2604 [
self setClientId:123 configuration:regularField];
2605 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2606 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2608 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2609 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2613 [textInputPlugin handleMethodCall:methodCall
2614 result:^(id _Nullable result){
2617 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2620 - (void)testDecommissionedViewAreNotReusedByAutofill {
2622 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2623 [configuration setValue:@{
2624 @"uniqueIdentifier" : @"field1",
2625 @"hints" : @[ UITextContentTypePassword ],
2626 @"editingValue" : @{@"text" : @""}
2628 forKey:@"autofill"];
2629 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2631 [
self setClientId:123 configuration:configuration];
2633 [
self setTextInputHide];
2636 [
self setClientId:124 configuration:configuration];
2640 XCTAssertNotNil(previousActiveView);
2644 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2651 #pragma mark - Accessibility - Tests
2653 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2654 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2657 [
self setTextInputShow];
2659 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2662 XCTAssertEqual([inputFields count], 1u);
2665 [
self setTextInputHide];
2667 inputFields =
self.installedInputViews;
2670 XCTAssertEqual([inputFields count], 0u);
2673 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2676 UIView* container = [[UIView alloc] init];
2677 UIAccessibilityElement* backing =
2678 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2679 inputView.backingTextInputAccessibilityObject = backing;
2682 [inputView accessibilityElementDidBecomeFocused];
2688 - (void)testFlutterTokenizerCanParseLines {
2690 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2693 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2694 XCTAssertEqual(range.
range.location, 0u);
2695 XCTAssertEqual(range.
range.length, 0u);
2697 [inputView insertText:@"how are you\nI am fine, Thank you"];
2699 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2700 XCTAssertEqual(range.
range.location, 0u);
2701 XCTAssertEqual(range.
range.length, 11u);
2703 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2704 XCTAssertEqual(range.
range.location, 0u);
2705 XCTAssertEqual(range.
range.length, 11u);
2707 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2708 XCTAssertEqual(range.
range.location, 0u);
2709 XCTAssertEqual(range.
range.length, 11u);
2711 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2712 XCTAssertEqual(range.
range.location, 12u);
2713 XCTAssertEqual(range.
range.length, 20u);
2715 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2716 XCTAssertEqual(range.
range.location, 12u);
2717 XCTAssertEqual(range.
range.length, 20u);
2719 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2720 XCTAssertEqual(range.
range.location, 12u);
2721 XCTAssertEqual(range.
range.length, 20u);
2724 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2726 [inputView insertText:@"0123456789\n012345"];
2727 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2730 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2731 withGranularity:UITextGranularityLine
2732 inDirection:UITextStorageDirectionBackward];
2733 XCTAssertEqual(range.
range.location, 11u);
2734 XCTAssertEqual(range.
range.length, 6u);
2737 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2739 [inputView insertText:@"0123456789\n012345"];
2740 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2743 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2744 withGranularity:UITextGranularityLine
2745 inDirection:UITextStorageDirectionForward];
2746 if (@available(iOS 17.0, *)) {
2747 XCTAssertNil(range);
2749 XCTAssertEqual(range.
range.location, 11u);
2750 XCTAssertEqual(range.
range.length, 6u);
2754 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2756 [inputView insertText:@"0123456789\n012345"];
2757 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2762 withGranularity:UITextGranularityLine
2763 inDirection:UITextStorageDirectionForward];
2764 if (@available(iOS 17.0, *)) {
2765 XCTAssertNil(range);
2767 XCTAssertEqual(range.
range.location, 0u);
2768 XCTAssertEqual(range.
range.length, 0u);
2772 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2777 __weak UIView* activeView;
2782 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2785 result:^(id _Nullable result){
2791 result:^(id _Nullable result){
2793 XCTAssertNotNil(activeView);
2796 XCTAssertNotNil(activeView);
2799 - (void)testFlutterTextInputPluginHostViewNilCrash {
2802 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2805 - (void)testFlutterTextInputPluginHostViewNotNil {
2811 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2814 - (void)testSetPlatformViewClient {
2821 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2823 result:^(id _Nullable result){
2826 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2829 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2831 result:^(id _Nullable result){
2833 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2836 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
2838 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2840 [inputView setTextInputClient:123];
2841 [inputView reloadInputViews];
2842 [inputView becomeFirstResponder];
2843 XCTAssert(inputView.isFirstResponder);
2845 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2846 [NSNotificationCenter.defaultCenter
2847 postNotificationName:UIKeyboardWillShowNotification
2849 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2853 [textInputPlugin handleMethodCall:onPointerMoveCall
2854 result:^(id _Nullable result){
2856 XCTAssertFalse(inputView.isFirstResponder);
2860 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
2861 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2862 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2863 UIScene* scene = scenes.anyObject;
2864 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2865 UIWindowScene* windowScene = (UIWindowScene*)scene;
2866 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2867 UIWindow* window = windowScene.windows[0];
2868 [window addSubview:viewController.view];
2870 [viewController loadView];
2873 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2875 [inputView setTextInputClient:123];
2876 [inputView reloadInputViews];
2877 [inputView becomeFirstResponder];
2880 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2881 [subView removeFromSuperview];
2885 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2886 [NSNotificationCenter.defaultCenter
2887 postNotificationName:UIKeyboardWillShowNotification
2889 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2893 [textInputPlugin handleMethodCall:onPointerMoveCall
2894 result:^(id _Nullable result){
2897 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2898 [subView removeFromSuperview];
2903 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
2904 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2905 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2906 UIScene* scene = scenes.anyObject;
2907 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2908 UIWindowScene* windowScene = (UIWindowScene*)scene;
2909 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2910 UIWindow* window = windowScene.windows[0];
2911 [window addSubview:viewController.view];
2913 [viewController loadView];
2916 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2918 [inputView setTextInputClient:123];
2919 [inputView reloadInputViews];
2920 [inputView becomeFirstResponder];
2922 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2923 [NSNotificationCenter.defaultCenter
2924 postNotificationName:UIKeyboardWillShowNotification
2926 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2930 [textInputPlugin handleMethodCall:onPointerMoveCall
2931 result:^(id _Nullable result){
2935 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2940 [textInputPlugin handleMethodCall:onPointerMoveCallMove
2941 result:^(id _Nullable result){
2945 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2947 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2948 [subView removeFromSuperview];
2953 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
2954 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2955 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2956 UIScene* scene = scenes.anyObject;
2957 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2958 UIWindowScene* windowScene = (UIWindowScene*)scene;
2959 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2960 UIWindow* window = windowScene.windows[0];
2961 [window addSubview:viewController.view];
2963 [viewController loadView];
2966 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2968 [inputView setTextInputClient:123];
2969 [inputView reloadInputViews];
2970 [inputView becomeFirstResponder];
2972 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2973 [NSNotificationCenter.defaultCenter
2974 postNotificationName:UIKeyboardWillShowNotification
2976 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2980 [textInputPlugin handleMethodCall:onPointerMoveCall
2981 result:^(id _Nullable result){
2984 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2989 [textInputPlugin handleMethodCall:onPointerMoveCallMove
2990 result:^(id _Nullable result){
2993 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2998 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
2999 result:^(id _Nullable result){
3002 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3003 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3004 [subView removeFromSuperview];
3009 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3011 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3012 [inputView setTextInputClient:123];
3013 [inputView reloadInputViews];
3014 [inputView becomeFirstResponder];
3016 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3017 XCTAssertEqualObjects(inputView, firstResponder);
3021 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3028 [subInputView addSubview:subFirstResponderInputView];
3029 [inputView addSubview:subInputView];
3030 [inputView addSubview:otherSubInputView];
3031 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3032 [inputView setTextInputClient:123];
3033 [inputView reloadInputViews];
3034 [subInputView setTextInputClient:123];
3035 [subInputView reloadInputViews];
3036 [otherSubInputView setTextInputClient:123];
3037 [otherSubInputView reloadInputViews];
3038 [subFirstResponderInputView setTextInputClient:123];
3039 [subFirstResponderInputView reloadInputViews];
3040 [subFirstResponderInputView becomeFirstResponder];
3042 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3043 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3047 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3049 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3050 [inputView setTextInputClient:123];
3051 [inputView reloadInputViews];
3053 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3054 XCTAssertNil(firstResponder);
3058 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3059 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3060 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3061 UIScene* scene = scenes.anyObject;
3062 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3063 UIWindowScene* windowScene = (UIWindowScene*)scene;
3064 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3065 UIWindow* window = windowScene.windows[0];
3066 [window addSubview:viewController.view];
3068 [viewController loadView];
3070 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3071 initWithDescription:
3072 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3073 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3074 .andDo(^(NSInvocation* invocation) {
3075 [expectation fulfill];
3077 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3078 [NSNotificationCenter.defaultCenter
3079 postNotificationName:UIKeyboardWillShowNotification
3081 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3085 [textInputPlugin handleMethodCall:initialMoveCall
3086 result:^(id _Nullable result){
3091 [textInputPlugin handleMethodCall:subsequentMoveCall
3092 result:^(id _Nullable result){
3098 [textInputPlugin handleMethodCall:pointerUpCall
3099 result:^(id _Nullable result){
3102 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3106 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3107 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3108 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3109 UIScene* scene = scenes.anyObject;
3110 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3111 UIWindowScene* windowScene = (UIWindowScene*)scene;
3112 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3113 UIWindow* window = windowScene.windows[0];
3114 [window addSubview:viewController.view];
3116 [viewController loadView];
3118 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3119 [NSNotificationCenter.defaultCenter
3120 postNotificationName:UIKeyboardWillShowNotification
3122 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3126 [textInputPlugin handleMethodCall:initialMoveCall
3127 result:^(id _Nullable result){
3132 [textInputPlugin handleMethodCall:subsequentMoveCall
3133 result:^(id _Nullable result){
3139 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3140 result:^(id _Nullable result){
3146 [textInputPlugin handleMethodCall:pointerUpCall
3147 result:^(id _Nullable result){
3149 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3150 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3152 XCTNSPredicateExpectation* expectation =
3153 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3154 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3158 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3159 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3160 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3161 UIScene* scene = scenes.anyObject;
3162 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3163 UIWindowScene* windowScene = (UIWindowScene*)scene;
3164 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3165 UIWindow* window = windowScene.windows[0];
3166 [window addSubview:viewController.view];
3168 [viewController loadView];
3171 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3173 [inputView setTextInputClient:123];
3174 [inputView reloadInputViews];
3175 [inputView becomeFirstResponder];
3177 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3178 [NSNotificationCenter.defaultCenter
3179 postNotificationName:UIKeyboardWillShowNotification
3181 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3185 [textInputPlugin handleMethodCall:initialMoveCall
3186 result:^(id _Nullable result){
3191 [textInputPlugin handleMethodCall:subsequentMoveCall
3192 result:^(id _Nullable result){
3198 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3199 result:^(id _Nullable result){
3205 [textInputPlugin handleMethodCall:pointerUpCall
3206 result:^(id _Nullable result){
3208 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3209 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3211 XCTNSPredicateExpectation* expectation =
3212 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3213 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3217 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3218 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3219 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3220 UIScene* scene = scenes.anyObject;
3221 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3222 UIWindowScene* windowScene = (UIWindowScene*)scene;
3223 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3224 UIWindow* window = windowScene.windows[0];
3225 [window addSubview:viewController.view];
3227 [viewController loadView];
3229 XCTestExpectation* expectation =
3230 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3231 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3232 [NSNotificationCenter.defaultCenter
3233 postNotificationName:UIKeyboardWillShowNotification
3235 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3239 [textInputPlugin handleMethodCall:initialMoveCall
3240 result:^(id _Nullable result){
3245 [textInputPlugin handleMethodCall:subsequentMoveCall
3246 result:^(id _Nullable result){
3251 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3252 result:^(id _Nullable result){
3259 handleMethodCall:pointerUpCall
3260 result:^(id _Nullable result) {
3261 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3262 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3263 keyboardFrame.origin.y);
3264 [expectation fulfill];
3269 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3270 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3271 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3272 UIScene* scene = scenes.anyObject;
3273 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3274 UIWindowScene* windowScene = (UIWindowScene*)scene;
3275 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3276 UIWindow* window = windowScene.windows[0];
3277 [window addSubview:viewController.view];
3279 [viewController loadView];
3281 XCTestExpectation* expectation =
3282 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3283 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3284 [NSNotificationCenter.defaultCenter
3285 postNotificationName:UIKeyboardWillShowNotification
3287 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3291 [textInputPlugin handleMethodCall:initialMoveCall
3292 result:^(id _Nullable result){
3297 [textInputPlugin handleMethodCall:subsequentMoveCall
3298 result:^(id _Nullable result){
3305 handleMethodCall:pointerUpCall
3306 result:^(id _Nullable result) {
3307 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3308 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3309 [expectation fulfill];
3313 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3314 [UIView setAnimationsEnabled:YES];
3315 [textInputPlugin showKeyboardAndRemoveScreenshot];
3317 UIView.areAnimationsEnabled,
3318 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3321 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3322 [UIView setAnimationsEnabled:YES];
3323 [textInputPlugin showKeyboardAndRemoveScreenshot];
3325 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3327 return UIView.areAnimationsEnabled;
3329 XCTNSPredicateExpectation* expectation =
3330 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3331 [
self waitForExpectations:@[ expectation ] timeout:10.0];