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 #pragma mark - TextEditingDelta tests
790 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
792 inputView.enableDeltaModel = YES;
794 __block
int updateCount = 0;
796 [inputView insertText:@"text to insert"];
799 flutterTextInputView:inputView
800 updateEditingClient:0
801 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
802 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
803 isEqualToString:
@""]) &&
804 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
805 isEqualToString:
@"text to insert"]) &&
806 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
807 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
809 .andDo(^(NSInvocation* invocation) {
812 XCTAssertEqual(updateCount, 0);
814 [
self flushScheduledAsyncBlocks];
817 XCTAssertEqual(updateCount, 1);
819 [inputView deleteBackward];
820 OCMExpect([
engine flutterTextInputView:inputView
821 updateEditingClient:0
822 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
823 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
824 isEqualToString:
@"text to insert"]) &&
825 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
826 isEqualToString:
@""]) &&
827 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
829 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
832 .andDo(^(NSInvocation* invocation) {
835 [
self flushScheduledAsyncBlocks];
836 XCTAssertEqual(updateCount, 2);
839 OCMExpect([
engine flutterTextInputView:inputView
840 updateEditingClient:0
841 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
842 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
843 isEqualToString:
@"text to inser"]) &&
844 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
845 isEqualToString:
@""]) &&
846 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
848 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
851 .andDo(^(NSInvocation* invocation) {
854 [
self flushScheduledAsyncBlocks];
855 XCTAssertEqual(updateCount, 3);
858 withText:@"replace text"];
861 flutterTextInputView:inputView
862 updateEditingClient:0
863 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
864 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
865 isEqualToString:
@"text to inser"]) &&
866 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
867 isEqualToString:
@"replace text"]) &&
868 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
869 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
871 .andDo(^(NSInvocation* invocation) {
874 [
self flushScheduledAsyncBlocks];
875 XCTAssertEqual(updateCount, 4);
877 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
878 OCMExpect([
engine flutterTextInputView:inputView
879 updateEditingClient:0
880 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
881 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
882 isEqualToString:
@"replace textext to inser"]) &&
883 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
884 isEqualToString:
@"marked text"]) &&
885 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
887 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
890 .andDo(^(NSInvocation* invocation) {
893 [
self flushScheduledAsyncBlocks];
894 XCTAssertEqual(updateCount, 5);
896 [inputView unmarkText];
898 flutterTextInputView:inputView
899 updateEditingClient:0
900 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
901 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
902 isEqualToString:
@"replace textmarked textext to inser"]) &&
903 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
904 isEqualToString:
@""]) &&
905 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
907 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
910 .andDo(^(NSInvocation* invocation) {
913 [
self flushScheduledAsyncBlocks];
915 XCTAssertEqual(updateCount, 6);
919 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
922 inputView.enableDeltaModel = YES;
925 OCMExpect([
engine flutterTextInputView:inputView
926 updateEditingClient:0
927 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
928 NSArray* deltas = state[@"deltas"];
929 NSDictionary* firstDelta = deltas[0];
930 NSDictionary* secondDelta = deltas[1];
931 NSDictionary* thirdDelta = deltas[2];
932 return [firstDelta[@"oldText"] isEqualToString:@""] &&
933 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
934 [firstDelta[@"deltaStart"] intValue] == 0 &&
935 [firstDelta[@"deltaEnd"] intValue] == 0 &&
936 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
937 [secondDelta[@"deltaText"] isEqualToString:@""] &&
938 [secondDelta[@"deltaStart"] intValue] == 0 &&
939 [secondDelta[@"deltaEnd"] intValue] == 1 &&
940 [thirdDelta[@"oldText"] isEqualToString:@""] &&
941 [thirdDelta[@"deltaText"] isEqualToString:@"â €”"] &&
942 [thirdDelta[@"deltaStart"] intValue] == 0 &&
943 [thirdDelta[@"deltaEnd"] intValue] == 0;
947 [inputView insertText:@"-"];
948 [inputView deleteBackward];
949 [inputView insertText:@"â €”"];
951 [
self flushScheduledAsyncBlocks];
955 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
957 inputView.enableDeltaModel = YES;
959 __block
int updateCount = 0;
960 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
961 .andDo(^(NSInvocation* invocation) {
965 [inputView.text setString:@"Some initial text"];
966 XCTAssertEqual(updateCount, 0);
969 inputView.markedTextRange = range;
970 inputView.selectedTextRange = nil;
971 [
self flushScheduledAsyncBlocks];
972 XCTAssertEqual(updateCount, 1);
974 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
976 flutterTextInputView:inputView
977 updateEditingClient:0
978 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
979 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
980 isEqualToString:
@"Some initial text"]) &&
981 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
982 isEqualToString:
@"new marked text."]) &&
983 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
984 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
986 [
self flushScheduledAsyncBlocks];
987 XCTAssertEqual(updateCount, 2);
990 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
992 inputView.enableDeltaModel = YES;
994 __block
int updateCount = 0;
995 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
996 .andDo(^(NSInvocation* invocation) {
1000 [inputView.text setString:@"Some initial text"];
1001 [
self flushScheduledAsyncBlocks];
1002 XCTAssertEqual(updateCount, 0);
1005 inputView.markedTextRange = range;
1006 inputView.selectedTextRange = nil;
1007 [
self flushScheduledAsyncBlocks];
1008 XCTAssertEqual(updateCount, 1);
1010 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1012 flutterTextInputView:inputView
1013 updateEditingClient:0
1014 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1015 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1016 isEqualToString:
@"Some initial text"]) &&
1017 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1018 isEqualToString:
@"text."]) &&
1019 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1020 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1022 [
self flushScheduledAsyncBlocks];
1023 XCTAssertEqual(updateCount, 2);
1026 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1028 inputView.enableDeltaModel = YES;
1030 __block
int updateCount = 0;
1031 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1032 .andDo(^(NSInvocation* invocation) {
1036 [inputView.text setString:@"Some initial text"];
1037 [
self flushScheduledAsyncBlocks];
1038 XCTAssertEqual(updateCount, 0);
1041 inputView.markedTextRange = range;
1042 inputView.selectedTextRange = nil;
1043 [
self flushScheduledAsyncBlocks];
1044 XCTAssertEqual(updateCount, 1);
1046 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1048 flutterTextInputView:inputView
1049 updateEditingClient:0
1050 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1051 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1052 isEqualToString:
@"Some initial text"]) &&
1053 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1054 isEqualToString:
@"tex"]) &&
1055 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1056 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1058 [
self flushScheduledAsyncBlocks];
1059 XCTAssertEqual(updateCount, 2);
1062 #pragma mark - EditingState tests
1064 - (void)testUITextInputCallsUpdateEditingStateOnce {
1067 __block
int updateCount = 0;
1068 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1069 .andDo(^(NSInvocation* invocation) {
1073 [inputView insertText:@"text to insert"];
1075 XCTAssertEqual(updateCount, 1);
1077 [inputView deleteBackward];
1078 XCTAssertEqual(updateCount, 2);
1081 XCTAssertEqual(updateCount, 3);
1084 withText:@"replace text"];
1085 XCTAssertEqual(updateCount, 4);
1087 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1088 XCTAssertEqual(updateCount, 5);
1090 [inputView unmarkText];
1091 XCTAssertEqual(updateCount, 6);
1094 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1096 inputView.enableDeltaModel = YES;
1098 __block
int updateCount = 0;
1099 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1100 .andDo(^(NSInvocation* invocation) {
1104 [inputView insertText:@"text to insert"];
1105 [
self flushScheduledAsyncBlocks];
1107 XCTAssertEqual(updateCount, 1);
1109 [inputView deleteBackward];
1110 [
self flushScheduledAsyncBlocks];
1111 XCTAssertEqual(updateCount, 2);
1114 [
self flushScheduledAsyncBlocks];
1115 XCTAssertEqual(updateCount, 3);
1118 withText:@"replace text"];
1119 [
self flushScheduledAsyncBlocks];
1120 XCTAssertEqual(updateCount, 4);
1122 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1123 [
self flushScheduledAsyncBlocks];
1124 XCTAssertEqual(updateCount, 5);
1126 [inputView unmarkText];
1127 [
self flushScheduledAsyncBlocks];
1128 XCTAssertEqual(updateCount, 6);
1131 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1134 __block
int updateCount = 0;
1135 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1136 .andDo(^(NSInvocation* invocation) {
1140 [inputView.text setString:@"BEFORE"];
1141 XCTAssertEqual(updateCount, 0);
1143 inputView.markedTextRange = nil;
1144 inputView.selectedTextRange = nil;
1145 XCTAssertEqual(updateCount, 1);
1148 XCTAssertEqual(updateCount, 1);
1149 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1150 XCTAssertEqual(updateCount, 1);
1151 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1152 XCTAssertEqual(updateCount, 1);
1156 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1157 XCTAssertEqual(updateCount, 1);
1159 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1160 XCTAssertEqual(updateCount, 1);
1164 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1165 XCTAssertEqual(updateCount, 1);
1167 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1168 XCTAssertEqual(updateCount, 1);
1171 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1173 inputView.enableDeltaModel = YES;
1175 __block
int updateCount = 0;
1176 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1177 .andDo(^(NSInvocation* invocation) {
1181 [inputView.text setString:@"BEFORE"];
1182 [
self flushScheduledAsyncBlocks];
1183 XCTAssertEqual(updateCount, 0);
1185 inputView.markedTextRange = nil;
1186 inputView.selectedTextRange = nil;
1187 [
self flushScheduledAsyncBlocks];
1188 XCTAssertEqual(updateCount, 1);
1191 XCTAssertEqual(updateCount, 1);
1192 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1193 [
self flushScheduledAsyncBlocks];
1194 XCTAssertEqual(updateCount, 1);
1196 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1197 [
self flushScheduledAsyncBlocks];
1198 XCTAssertEqual(updateCount, 1);
1202 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1203 [
self flushScheduledAsyncBlocks];
1204 XCTAssertEqual(updateCount, 1);
1207 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1208 [
self flushScheduledAsyncBlocks];
1209 XCTAssertEqual(updateCount, 1);
1213 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1214 [
self flushScheduledAsyncBlocks];
1215 XCTAssertEqual(updateCount, 1);
1218 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1219 [
self flushScheduledAsyncBlocks];
1220 XCTAssertEqual(updateCount, 1);
1223 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1226 __block
int updateCount = 0;
1227 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1228 .andDo(^(NSInvocation* invocation) {
1232 [inputView unmarkText];
1234 XCTAssertEqual(updateCount, 0);
1236 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1238 XCTAssertEqual(updateCount, 1);
1240 [inputView unmarkText];
1242 XCTAssertEqual(updateCount, 2);
1245 - (void)testCanCopyPasteWithScribbleEnabled {
1246 if (@available(iOS 14.0, *)) {
1247 NSDictionary* config =
self.mutableTemplateCopy;
1248 [
self setClientId:123 configuration:config];
1249 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1255 [mockInputView insertText:@"aaaa"];
1256 [mockInputView selectAll:nil];
1258 XCTAssertFalse([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1259 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1260 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1261 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1263 [mockInputView copy:NULL];
1264 XCTAssertFalse([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1265 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1266 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1267 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1271 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1272 if (@available(iOS 14.0, *)) {
1275 __block
int updateCount = 0;
1276 OCMStub([
engine flutterTextInputView:inputView
1277 updateEditingClient:0
1278 withState:[OCMArg isNotNil]])
1279 .andDo(^(NSInvocation* invocation) {
1283 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1285 XCTAssertEqual(updateCount, 1);
1287 UIScribbleInteraction* scribbleInteraction =
1288 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1290 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1291 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1293 XCTAssertEqual(updateCount, 1);
1295 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1296 [inputView resetScribbleInteractionStatusIfEnding];
1297 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1299 XCTAssertEqual(updateCount, 2);
1301 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1302 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1305 XCTAssertEqual(updateCount, 2);
1307 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1308 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1311 XCTAssertEqual(updateCount, 2);
1313 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1314 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1316 XCTAssertEqual(updateCount, 3);
1320 - (void)testUpdateEditingClientNegativeSelection {
1323 [inputView.text setString:@"SELECTION"];
1324 inputView.markedTextRange = nil;
1325 inputView.selectedTextRange = nil;
1327 [inputView setTextInputState:@{
1328 @"text" : @"SELECTION",
1329 @"selectionBase" : @-1,
1330 @"selectionExtent" : @-1
1332 [inputView updateEditingState];
1333 OCMVerify([
engine flutterTextInputView:inputView
1334 updateEditingClient:0
1335 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1336 return ([state[
@"selectionBase"] intValue]) == 0 &&
1337 ([state[
@"selectionExtent"] intValue] == 0);
1342 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1343 [inputView updateEditingState];
1344 OCMVerify([
engine flutterTextInputView:inputView
1345 updateEditingClient:0
1346 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1347 return ([state[
@"selectionBase"] intValue]) == 0 &&
1348 ([state[
@"selectionExtent"] intValue] == 0);
1352 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1353 [inputView updateEditingState];
1354 OCMVerify([
engine flutterTextInputView:inputView
1355 updateEditingClient:0
1356 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1357 return ([state[
@"selectionBase"] intValue]) == 0 &&
1358 ([state[
@"selectionExtent"] intValue] == 0);
1362 - (void)testUpdateEditingClientSelectionClamping {
1366 [inputView.text setString:@"SELECTION"];
1367 inputView.markedTextRange = nil;
1368 inputView.selectedTextRange = nil;
1371 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1372 [inputView updateEditingState];
1373 OCMVerify([
engine flutterTextInputView:inputView
1374 updateEditingClient:0
1375 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1376 return ([state[
@"selectionBase"] intValue]) == 0 &&
1377 ([state[
@"selectionExtent"] intValue] == 0);
1381 [inputView setTextInputState:@{
1382 @"text" : @"SELECTION",
1383 @"selectionBase" : @0,
1384 @"selectionExtent" : @9999
1386 [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] == 9);
1397 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1398 [inputView updateEditingState];
1399 OCMVerify([
engine flutterTextInputView:inputView
1400 updateEditingClient:0
1401 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1402 return ([state[
@"selectionBase"] intValue]) == 0 &&
1403 ([state[
@"selectionExtent"] intValue] == 1);
1407 [inputView setTextInputState:@{
1408 @"text" : @"SELECTION",
1409 @"selectionBase" : @9999,
1410 @"selectionExtent" : @9999
1412 [inputView updateEditingState];
1413 OCMVerify([
engine flutterTextInputView:inputView
1414 updateEditingClient:0
1415 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1416 return ([state[
@"selectionBase"] intValue]) == 9 &&
1417 ([state[
@"selectionExtent"] intValue] == 9);
1421 - (void)testInputViewsHasNonNilInputDelegate {
1422 if (@available(iOS 13.0, *)) {
1424 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1426 [inputView setTextInputClient:123];
1427 [inputView reloadInputViews];
1428 [inputView becomeFirstResponder];
1429 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1430 inputView.inputDelegate = nil;
1433 [mockInputView setTextInputState:@{
1434 @"text" : @"COMPOSING",
1435 @"composingBase" : @1,
1436 @"composingExtent" : @3
1438 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1439 [inputView removeFromSuperview];
1443 - (void)testInputViewsDoNotHaveUITextInteractions {
1444 if (@available(iOS 13.0, *)) {
1446 BOOL hasTextInteraction = NO;
1447 for (
id interaction in inputView.interactions) {
1448 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1449 if (hasTextInteraction) {
1453 XCTAssertFalse(hasTextInteraction);
1457 #pragma mark - UITextInput methods - Tests
1459 - (void)testUpdateFirstRectForRange {
1460 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1466 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1471 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1472 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1476 NSArray* affineMatrix = @[
1477 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1478 @(-6.0), @(3.0), @(9.0), @(1.0)
1482 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1484 [inputView setEditableTransform:yOffsetMatrix];
1486 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1489 CGRect testRect = CGRectMake(0, 0, 100, 100);
1490 [inputView setMarkedRect:testRect];
1492 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1493 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1495 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1498 [inputView setEditableTransform:zeroMatrix];
1500 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1501 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1504 [inputView setEditableTransform:yOffsetMatrix];
1505 [inputView setMarkedRect:testRect];
1506 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1509 [inputView setMarkedRect:kInvalidFirstRect];
1511 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1512 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1515 [inputView setEditableTransform:affineMatrix];
1516 [inputView setMarkedRect:testRect];
1518 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1520 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1521 const CGPoint offset = CGPointMake(113, 119);
1522 CGRect currentFrame = inputView.frame;
1523 currentFrame.origin = offset;
1524 inputView.frame = currentFrame;
1527 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1528 [inputView firstRectForRange:range]));
1531 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1533 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1535 [inputView setSelectionRects:@[
1542 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1543 [inputView firstRectForRange:singleRectRange]));
1547 if (@available(iOS 17, *)) {
1548 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1549 [inputView firstRectForRange:multiRectRange]));
1551 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1552 [inputView firstRectForRange:multiRectRange]));
1555 [inputView setTextInputState:@{@"text" : @"COM"}];
1557 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1560 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1562 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1564 [inputView setSelectionRects:@[
1571 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1572 [inputView firstRectForRange:singleRectRange]));
1575 if (@available(iOS 17, *)) {
1576 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1577 [inputView firstRectForRange:multiRectRange]));
1579 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1580 [inputView firstRectForRange:multiRectRange]));
1583 [inputView setTextInputState:@{@"text" : @"COM"}];
1585 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1588 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1590 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1592 [inputView setSelectionRects:@[
1603 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1604 [inputView firstRectForRange:singleRectRange]));
1608 if (@available(iOS 17, *)) {
1609 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1610 [inputView firstRectForRange:multiRectRange]));
1612 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1613 [inputView firstRectForRange:multiRectRange]));
1617 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1619 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1621 [inputView setSelectionRects:@[
1632 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1633 [inputView firstRectForRange:singleRectRange]));
1636 if (@available(iOS 17, *)) {
1637 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1638 [inputView firstRectForRange:multiRectRange]));
1640 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1641 [inputView firstRectForRange:multiRectRange]));
1645 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1647 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1649 [inputView setSelectionRects:@[
1660 if (@available(iOS 17, *)) {
1661 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1662 [inputView firstRectForRange:multiRectRange]));
1664 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80),
1665 [inputView firstRectForRange:multiRectRange]));
1669 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1671 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1673 [inputView setSelectionRects:@[
1684 if (@available(iOS 17, *)) {
1685 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1686 [inputView firstRectForRange:multiRectRange]));
1688 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120),
1689 [inputView firstRectForRange:multiRectRange]));
1693 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1695 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1697 [inputView setSelectionRects:@[
1708 if (@available(iOS 17, *)) {
1709 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1710 [inputView firstRectForRange:multiRectRange]));
1712 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1713 [inputView firstRectForRange:multiRectRange]));
1717 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1719 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1721 [inputView setSelectionRects:@[
1732 if (@available(iOS 17, *)) {
1733 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1734 [inputView firstRectForRange:multiRectRange]));
1736 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1737 [inputView firstRectForRange:multiRectRange]));
1741 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1743 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1745 [inputView setSelectionRects:@[
1756 if (@available(iOS 17, *)) {
1757 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1758 [inputView firstRectForRange:multiRectRange]));
1760 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1761 [inputView firstRectForRange:multiRectRange]));
1765 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1767 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1769 [inputView setSelectionRects:@[
1780 if (@available(iOS 17, *)) {
1781 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1782 [inputView firstRectForRange:multiRectRange]));
1784 XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100),
1785 [inputView firstRectForRange:multiRectRange]));
1789 - (void)testClosestPositionToPoint {
1791 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1794 [inputView setSelectionRects:@[
1799 CGPoint point = CGPointMake(150, 150);
1800 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1801 XCTAssertEqual(UITextStorageDirectionBackward,
1806 [inputView setSelectionRects:@[
1813 point = CGPointMake(125, 150);
1814 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1815 XCTAssertEqual(UITextStorageDirectionForward,
1820 [inputView setSelectionRects:@[
1827 point = CGPointMake(125, 201);
1828 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1829 XCTAssertEqual(UITextStorageDirectionBackward,
1833 [inputView setSelectionRects:@[
1839 point = CGPointMake(125, 250);
1840 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1841 XCTAssertEqual(UITextStorageDirectionBackward,
1845 [inputView setSelectionRects:@[
1850 point = CGPointMake(110, 50);
1851 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1852 XCTAssertEqual(UITextStorageDirectionForward,
1857 [inputView beginFloatingCursorAtPoint:CGPointZero];
1858 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1859 XCTAssertEqual(UITextStorageDirectionForward,
1861 [inputView endFloatingCursor];
1864 - (void)testClosestPositionToPointRTL {
1866 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1868 [inputView setSelectionRects:@[
1884 XCTAssertEqual(0U, position.
index);
1885 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1887 XCTAssertEqual(1U, position.
index);
1888 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1890 XCTAssertEqual(1U, position.
index);
1891 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1893 XCTAssertEqual(2U, position.
index);
1894 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1896 XCTAssertEqual(2U, position.
index);
1897 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
1899 XCTAssertEqual(3U, position.
index);
1900 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1902 XCTAssertEqual(3U, position.
index);
1903 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
1906 - (void)testSelectionRectsForRange {
1908 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1910 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
1911 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
1912 [inputView setSelectionRects:@[
1921 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
1922 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
1923 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
1927 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
1928 XCTAssertTrue(CGRectEqualToRect(
1929 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
1930 [inputView selectionRectsForRange:range][0].rect));
1933 - (void)testClosestPositionToPointWithinRange {
1935 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1938 [inputView setSelectionRects:@[
1945 CGPoint point = CGPointMake(125, 150);
1948 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
1950 UITextStorageDirectionForward,
1951 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
1954 [inputView setSelectionRects:@[
1961 point = CGPointMake(125, 150);
1964 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
1966 UITextStorageDirectionForward,
1967 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
1970 - (void)testClosestPositionToPointWithPartialSelectionRects {
1972 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1979 XCTAssertTrue(CGRectEqualToRect(
1982 affinity:UITextStorageDirectionForward]],
1983 CGRectMake(100, 0, 0, 100)));
1986 XCTAssertTrue(CGRectEqualToRect(
1989 affinity:UITextStorageDirectionForward]],
1993 #pragma mark - Floating Cursor - Tests
1995 - (void)testFloatingCursorDoesNotThrow {
1998 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
1999 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2000 [inputView endFloatingCursor];
2001 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2002 [inputView endFloatingCursor];
2005 - (void)testFloatingCursor {
2007 [inputView setTextInputState:@{
2009 @"selectionBase" : @1,
2010 @"selectionExtent" : @1,
2021 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2024 XCTAssertTrue(CGRectEqualToRect(
2027 affinity:UITextStorageDirectionForward]],
2028 CGRectMake(0, 0, 0, 100)));
2031 XCTAssertTrue(CGRectEqualToRect(
2034 affinity:UITextStorageDirectionForward]],
2035 CGRectMake(100, 100, 0, 100)));
2036 XCTAssertTrue(CGRectEqualToRect(
2039 affinity:UITextStorageDirectionForward]],
2040 CGRectMake(200, 200, 0, 100)));
2041 XCTAssertTrue(CGRectEqualToRect(
2044 affinity:UITextStorageDirectionForward]],
2045 CGRectMake(300, 300, 0, 100)));
2048 XCTAssertTrue(CGRectEqualToRect(
2051 affinity:UITextStorageDirectionForward]],
2052 CGRectMake(400, 300, 0, 100)));
2054 XCTAssertTrue(CGRectEqualToRect(
2057 affinity:UITextStorageDirectionForward]],
2061 [inputView setTextInputState:@{
2063 @"selectionBase" : @2,
2064 @"selectionExtent" : @2,
2067 XCTAssertTrue(CGRectEqualToRect(
2070 affinity:UITextStorageDirectionBackward]],
2071 CGRectMake(0, 0, 0, 100)));
2074 XCTAssertTrue(CGRectEqualToRect(
2077 affinity:UITextStorageDirectionBackward]],
2078 CGRectMake(100, 0, 0, 100)));
2079 XCTAssertTrue(CGRectEqualToRect(
2082 affinity:UITextStorageDirectionBackward]],
2083 CGRectMake(200, 100, 0, 100)));
2084 XCTAssertTrue(CGRectEqualToRect(
2087 affinity:UITextStorageDirectionBackward]],
2088 CGRectMake(300, 200, 0, 100)));
2089 XCTAssertTrue(CGRectEqualToRect(
2092 affinity:UITextStorageDirectionBackward]],
2093 CGRectMake(400, 300, 0, 100)));
2095 XCTAssertTrue(CGRectEqualToRect(
2098 affinity:UITextStorageDirectionBackward]],
2103 CGRect initialBounds = inputView.bounds;
2104 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2105 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2106 OCMVerify([
engine flutterTextInputView:inputView
2107 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2109 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2110 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2111 ([state[
@"Y"] isEqualToNumber:@(0)]);
2114 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2115 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2116 OCMVerify([
engine flutterTextInputView:inputView
2117 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2119 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2120 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2121 ([state[
@"Y"] isEqualToNumber:@(333)]);
2124 [inputView endFloatingCursor];
2125 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2126 OCMVerify([
engine flutterTextInputView:inputView
2127 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2129 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2130 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2131 ([state[
@"Y"] isEqualToNumber:@(0)]);
2135 #pragma mark - UIKeyInput Overrides - Tests
2137 - (void)testInsertTextAddsPlaceholderSelectionRects {
2140 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2150 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2153 [inputView insertText:@"in"];
2181 #pragma mark - Autofill - Utilities
2183 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2186 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2187 @"keyboardAppearance" :
@"Brightness.light",
2188 @"obscureText" : @YES,
2189 @"inputAction" :
@"TextInputAction.unspecified",
2190 @"smartDashesType" :
@"0",
2191 @"smartQuotesType" :
@"0",
2192 @"autocorrect" : @YES
2196 return [_passwordTemplate mutableCopy];
2200 return [
self.installedInputViews
2201 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2204 - (void)commitAutofillContextAndVerify {
2208 [textInputPlugin handleMethodCall:methodCall
2209 result:^(id _Nullable result){
2212 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2217 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2221 #pragma mark - Autofill - Tests
2223 - (void)testDisablingAutofillOnInputClient {
2224 NSDictionary* config =
self.mutableTemplateCopy;
2225 [config setValue:@"YES" forKey:@"obscureText"];
2227 [
self setClientId:123 configuration:config];
2230 XCTAssertEqualObjects(inputView.textContentType,
@"");
2233 - (void)testAutofillEnabledByDefault {
2234 NSDictionary* config =
self.mutableTemplateCopy;
2235 [config setValue:@"NO" forKey:@"obscureText"];
2236 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2237 forKey:@"autofill"];
2239 [
self setClientId:123 configuration:config];
2242 XCTAssertNil(inputView.textContentType);
2245 - (void)testAutofillContext {
2246 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2249 @"uniqueIdentifier" : @"field1",
2250 @"hints" : @[ @"hint1" ],
2251 @"editingValue" : @{@"text" : @""}
2253 forKey:@"autofill"];
2255 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2257 @"uniqueIdentifier" : @"field2",
2258 @"hints" : @[ @"hint2" ],
2259 @"editingValue" : @{@"text" : @""}
2261 forKey:@"autofill"];
2263 NSMutableDictionary* config = [field1 mutableCopy];
2264 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2266 [
self setClientId:123 configuration:config];
2267 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2271 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2272 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2274 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2277 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2279 @"uniqueIdentifier" : @"field3",
2280 @"hints" : @[ @"hint3" ],
2281 @"editingValue" : @{@"text" : @""}
2283 forKey:@"autofill"];
2287 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2289 [
self setClientId:123 configuration:config];
2291 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2294 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2295 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2297 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2300 for (NSString* key in oldContext.allKeys) {
2301 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2305 config =
self.mutablePasswordTemplateCopy;
2308 [
self setClientId:124 configuration:config];
2309 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2311 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2314 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2315 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2318 for (NSString* key in oldContext.allKeys) {
2319 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2323 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2327 [
self setClientId:200 configuration:config];
2330 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2333 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2334 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2337 for (NSString* key in oldContext.allKeys) {
2338 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2341 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2344 - (void)testCommitAutofillContext {
2345 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2347 @"uniqueIdentifier" : @"field1",
2348 @"hints" : @[ @"hint1" ],
2349 @"editingValue" : @{@"text" : @""}
2351 forKey:@"autofill"];
2353 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2355 @"uniqueIdentifier" : @"field2",
2356 @"hints" : @[ @"hint2" ],
2357 @"editingValue" : @{@"text" : @""}
2359 forKey:@"autofill"];
2361 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2363 @"uniqueIdentifier" : @"field3",
2364 @"hints" : @[ @"hint3" ],
2365 @"editingValue" : @{@"text" : @""}
2367 forKey:@"autofill"];
2369 NSMutableDictionary* config = [field1 mutableCopy];
2370 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2372 [
self setClientId:123 configuration:config];
2373 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2375 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2377 [
self commitAutofillContextAndVerify];
2378 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2381 [
self setClientId:123 configuration:config];
2383 [
self setClientId:124 configuration:field3];
2384 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2386 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2387 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2390 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2392 [
self commitAutofillContextAndVerify];
2393 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2396 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2398 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2402 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2403 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2405 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2407 [
self commitAutofillContextAndVerify];
2408 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2411 - (void)testAutofillInputViews {
2412 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2414 @"uniqueIdentifier" : @"field1",
2415 @"hints" : @[ @"hint1" ],
2416 @"editingValue" : @{@"text" : @""}
2418 forKey:@"autofill"];
2420 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2422 @"uniqueIdentifier" : @"field2",
2423 @"hints" : @[ @"hint2" ],
2424 @"editingValue" : @{@"text" : @""}
2426 forKey:@"autofill"];
2428 NSMutableDictionary* config = [field1 mutableCopy];
2429 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2431 [
self setClientId:123 configuration:config];
2432 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2435 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2438 XCTAssertEqual(inputFields.count, 2ul);
2439 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2444 withText:@"Autofilled!"];
2445 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2448 OCMVerify([
engine flutterTextInputView:inactiveView
2449 updateEditingClient:0
2450 withState:[OCMArg isNotNil]
2451 withTag:
@"field2"]);
2454 - (void)testPasswordAutofillHack {
2455 NSDictionary* config =
self.mutableTemplateCopy;
2456 [config setValue:@"YES" forKey:@"obscureText"];
2457 [
self setClientId:123 configuration:config];
2460 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2464 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2467 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2470 - (void)testClearAutofillContextClearsSelection {
2471 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2472 NSDictionary* editingValue = @{
2473 @"text" :
@"REGULAR_TEXT_FIELD",
2474 @"composingBase" : @0,
2475 @"composingExtent" : @3,
2476 @"selectionBase" : @1,
2477 @"selectionExtent" : @4
2479 [regularField setValue:@{
2480 @"uniqueIdentifier" : @"field2",
2481 @"hints" : @[ @"hint2" ],
2482 @"editingValue" : editingValue,
2484 forKey:@"autofill"];
2485 [regularField addEntriesFromDictionary:editingValue];
2486 [
self setClientId:123 configuration:regularField];
2487 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2488 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2491 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2493 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2497 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2498 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2500 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2502 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2503 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2506 XCTAssert([oldInputView.text isEqualToString:
@""]);
2508 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2511 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2513 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2514 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2516 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2519 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2520 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2522 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2524 [
self commitAutofillContextAndVerify];
2527 - (void)testScribbleSetSelectionRects {
2528 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2529 NSDictionary* editingValue = @{
2530 @"text" :
@"REGULAR_TEXT_FIELD",
2531 @"composingBase" : @0,
2532 @"composingExtent" : @3,
2533 @"selectionBase" : @1,
2534 @"selectionExtent" : @4
2536 [regularField setValue:@{
2537 @"uniqueIdentifier" : @"field1",
2538 @"hints" : @[ @"hint2" ],
2539 @"editingValue" : editingValue,
2541 forKey:@"autofill"];
2542 [regularField addEntriesFromDictionary:editingValue];
2543 [
self setClientId:123 configuration:regularField];
2544 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2545 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2547 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2548 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2552 [textInputPlugin handleMethodCall:methodCall
2553 result:^(id _Nullable result){
2556 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2559 - (void)testDecommissionedViewAreNotReusedByAutofill {
2561 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2562 [configuration setValue:@{
2563 @"uniqueIdentifier" : @"field1",
2564 @"hints" : @[ UITextContentTypePassword ],
2565 @"editingValue" : @{@"text" : @""}
2567 forKey:@"autofill"];
2568 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2570 [
self setClientId:123 configuration:configuration];
2572 [
self setTextInputHide];
2575 [
self setClientId:124 configuration:configuration];
2579 XCTAssertNotNil(previousActiveView);
2583 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2590 #pragma mark - Accessibility - Tests
2592 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2593 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2596 [
self setTextInputShow];
2598 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2601 XCTAssertEqual([inputFields count], 1u);
2604 [
self setTextInputHide];
2606 inputFields =
self.installedInputViews;
2609 XCTAssertEqual([inputFields count], 0u);
2612 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2615 UIView* container = [[UIView alloc] init];
2616 UIAccessibilityElement* backing =
2617 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2618 inputView.backingTextInputAccessibilityObject = backing;
2621 [inputView accessibilityElementDidBecomeFocused];
2627 - (void)testFlutterTokenizerCanParseLines {
2629 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2632 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2633 XCTAssertEqual(range.
range.location, 0u);
2634 XCTAssertEqual(range.
range.length, 0u);
2636 [inputView insertText:@"how are you\nI am fine, Thank you"];
2638 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2639 XCTAssertEqual(range.
range.location, 0u);
2640 XCTAssertEqual(range.
range.length, 11u);
2642 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2643 XCTAssertEqual(range.
range.location, 0u);
2644 XCTAssertEqual(range.
range.length, 11u);
2646 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2647 XCTAssertEqual(range.
range.location, 0u);
2648 XCTAssertEqual(range.
range.length, 11u);
2650 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2651 XCTAssertEqual(range.
range.location, 12u);
2652 XCTAssertEqual(range.
range.length, 20u);
2654 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2655 XCTAssertEqual(range.
range.location, 12u);
2656 XCTAssertEqual(range.
range.length, 20u);
2658 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2659 XCTAssertEqual(range.
range.location, 12u);
2660 XCTAssertEqual(range.
range.length, 20u);
2663 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2665 [inputView insertText:@"0123456789\n012345"];
2666 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2669 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2670 withGranularity:UITextGranularityLine
2671 inDirection:UITextStorageDirectionBackward];
2672 XCTAssertEqual(range.
range.location, 11u);
2673 XCTAssertEqual(range.
range.length, 6u);
2676 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2678 [inputView insertText:@"0123456789\n012345"];
2679 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2682 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2683 withGranularity:UITextGranularityLine
2684 inDirection:UITextStorageDirectionForward];
2685 if (@available(iOS 17.0, *)) {
2686 XCTAssertNil(range);
2688 XCTAssertEqual(range.
range.location, 11u);
2689 XCTAssertEqual(range.
range.length, 6u);
2693 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2695 [inputView insertText:@"0123456789\n012345"];
2696 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2701 withGranularity:UITextGranularityLine
2702 inDirection:UITextStorageDirectionForward];
2703 if (@available(iOS 17.0, *)) {
2704 XCTAssertNil(range);
2706 XCTAssertEqual(range.
range.location, 0u);
2707 XCTAssertEqual(range.
range.length, 0u);
2711 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2716 __weak UIView* activeView;
2721 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2724 result:^(id _Nullable result){
2730 result:^(id _Nullable result){
2732 XCTAssertNotNil(activeView);
2735 XCTAssertNotNil(activeView);
2738 - (void)testFlutterTextInputPluginHostViewNilCrash {
2741 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2744 - (void)testFlutterTextInputPluginHostViewNotNil {
2750 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2753 - (void)testSetPlatformViewClient {
2760 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2762 result:^(id _Nullable result){
2765 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2768 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2770 result:^(id _Nullable result){
2772 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
2775 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
2777 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2779 [inputView setTextInputClient:123];
2780 [inputView reloadInputViews];
2781 [inputView becomeFirstResponder];
2782 XCTAssert(inputView.isFirstResponder);
2784 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2785 [NSNotificationCenter.defaultCenter
2786 postNotificationName:UIKeyboardWillShowNotification
2788 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2792 [textInputPlugin handleMethodCall:onPointerMoveCall
2793 result:^(id _Nullable result){
2795 XCTAssertFalse(inputView.isFirstResponder);
2799 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
2800 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2801 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2802 UIScene* scene = scenes.anyObject;
2803 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2804 UIWindowScene* windowScene = (UIWindowScene*)scene;
2805 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2806 UIWindow* window = windowScene.windows[0];
2807 [window addSubview:viewController.view];
2809 [viewController loadView];
2812 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2814 [inputView setTextInputClient:123];
2815 [inputView reloadInputViews];
2816 [inputView becomeFirstResponder];
2819 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2820 [subView removeFromSuperview];
2824 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2825 [NSNotificationCenter.defaultCenter
2826 postNotificationName:UIKeyboardWillShowNotification
2828 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2832 [textInputPlugin handleMethodCall:onPointerMoveCall
2833 result:^(id _Nullable result){
2836 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2837 [subView removeFromSuperview];
2842 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
2843 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2844 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2845 UIScene* scene = scenes.anyObject;
2846 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2847 UIWindowScene* windowScene = (UIWindowScene*)scene;
2848 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2849 UIWindow* window = windowScene.windows[0];
2850 [window addSubview:viewController.view];
2852 [viewController loadView];
2855 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2857 [inputView setTextInputClient:123];
2858 [inputView reloadInputViews];
2859 [inputView becomeFirstResponder];
2861 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2862 [NSNotificationCenter.defaultCenter
2863 postNotificationName:UIKeyboardWillShowNotification
2865 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2869 [textInputPlugin handleMethodCall:onPointerMoveCall
2870 result:^(id _Nullable result){
2874 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2879 [textInputPlugin handleMethodCall:onPointerMoveCallMove
2880 result:^(id _Nullable result){
2884 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2886 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2887 [subView removeFromSuperview];
2892 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
2893 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2894 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
2895 UIScene* scene = scenes.anyObject;
2896 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
2897 UIWindowScene* windowScene = (UIWindowScene*)scene;
2898 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
2899 UIWindow* window = windowScene.windows[0];
2900 [window addSubview:viewController.view];
2902 [viewController loadView];
2905 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2907 [inputView setTextInputClient:123];
2908 [inputView reloadInputViews];
2909 [inputView becomeFirstResponder];
2911 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2912 [NSNotificationCenter.defaultCenter
2913 postNotificationName:UIKeyboardWillShowNotification
2915 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2919 [textInputPlugin handleMethodCall:onPointerMoveCall
2920 result:^(id _Nullable result){
2923 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2928 [textInputPlugin handleMethodCall:onPointerMoveCallMove
2929 result:^(id _Nullable result){
2932 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2937 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
2938 result:^(id _Nullable result){
2941 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2942 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
2943 [subView removeFromSuperview];
2948 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
2950 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2951 [inputView setTextInputClient:123];
2952 [inputView reloadInputViews];
2953 [inputView becomeFirstResponder];
2955 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2956 XCTAssertEqualObjects(inputView, firstResponder);
2960 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
2967 [subInputView addSubview:subFirstResponderInputView];
2968 [inputView addSubview:subInputView];
2969 [inputView addSubview:otherSubInputView];
2970 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2971 [inputView setTextInputClient:123];
2972 [inputView reloadInputViews];
2973 [subInputView setTextInputClient:123];
2974 [subInputView reloadInputViews];
2975 [otherSubInputView setTextInputClient:123];
2976 [otherSubInputView reloadInputViews];
2977 [subFirstResponderInputView setTextInputClient:123];
2978 [subFirstResponderInputView reloadInputViews];
2979 [subFirstResponderInputView becomeFirstResponder];
2981 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2982 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
2986 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
2988 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2989 [inputView setTextInputClient:123];
2990 [inputView reloadInputViews];
2992 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2993 XCTAssertNil(firstResponder);
2997 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
2998 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2999 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3000 UIScene* scene = scenes.anyObject;
3001 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3002 UIWindowScene* windowScene = (UIWindowScene*)scene;
3003 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3004 UIWindow* window = windowScene.windows[0];
3005 [window addSubview:viewController.view];
3007 [viewController loadView];
3009 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3010 initWithDescription:
3011 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3012 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3013 .andDo(^(NSInvocation* invocation) {
3014 [expectation fulfill];
3016 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3017 [NSNotificationCenter.defaultCenter
3018 postNotificationName:UIKeyboardWillShowNotification
3020 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3024 [textInputPlugin handleMethodCall:initialMoveCall
3025 result:^(id _Nullable result){
3030 [textInputPlugin handleMethodCall:subsequentMoveCall
3031 result:^(id _Nullable result){
3037 [textInputPlugin handleMethodCall:pointerUpCall
3038 result:^(id _Nullable result){
3041 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3045 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3046 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3047 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3048 UIScene* scene = scenes.anyObject;
3049 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3050 UIWindowScene* windowScene = (UIWindowScene*)scene;
3051 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3052 UIWindow* window = windowScene.windows[0];
3053 [window addSubview:viewController.view];
3055 [viewController loadView];
3057 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3058 [NSNotificationCenter.defaultCenter
3059 postNotificationName:UIKeyboardWillShowNotification
3061 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3065 [textInputPlugin handleMethodCall:initialMoveCall
3066 result:^(id _Nullable result){
3071 [textInputPlugin handleMethodCall:subsequentMoveCall
3072 result:^(id _Nullable result){
3078 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3079 result:^(id _Nullable result){
3085 [textInputPlugin handleMethodCall:pointerUpCall
3086 result:^(id _Nullable result){
3088 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3089 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3091 XCTNSPredicateExpectation* expectation =
3092 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3093 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3097 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3098 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3099 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3100 UIScene* scene = scenes.anyObject;
3101 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3102 UIWindowScene* windowScene = (UIWindowScene*)scene;
3103 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3104 UIWindow* window = windowScene.windows[0];
3105 [window addSubview:viewController.view];
3107 [viewController loadView];
3110 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3112 [inputView setTextInputClient:123];
3113 [inputView reloadInputViews];
3114 [inputView becomeFirstResponder];
3116 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3117 [NSNotificationCenter.defaultCenter
3118 postNotificationName:UIKeyboardWillShowNotification
3120 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3124 [textInputPlugin handleMethodCall:initialMoveCall
3125 result:^(id _Nullable result){
3130 [textInputPlugin handleMethodCall:subsequentMoveCall
3131 result:^(id _Nullable result){
3137 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3138 result:^(id _Nullable result){
3144 [textInputPlugin handleMethodCall:pointerUpCall
3145 result:^(id _Nullable result){
3147 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3148 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3150 XCTNSPredicateExpectation* expectation =
3151 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3152 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3156 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3157 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3158 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3159 UIScene* scene = scenes.anyObject;
3160 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3161 UIWindowScene* windowScene = (UIWindowScene*)scene;
3162 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3163 UIWindow* window = windowScene.windows[0];
3164 [window addSubview:viewController.view];
3166 [viewController loadView];
3168 XCTestExpectation* expectation =
3169 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3170 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3171 [NSNotificationCenter.defaultCenter
3172 postNotificationName:UIKeyboardWillShowNotification
3174 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3178 [textInputPlugin handleMethodCall:initialMoveCall
3179 result:^(id _Nullable result){
3184 [textInputPlugin handleMethodCall:subsequentMoveCall
3185 result:^(id _Nullable result){
3190 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3191 result:^(id _Nullable result){
3198 handleMethodCall:pointerUpCall
3199 result:^(id _Nullable result) {
3200 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3201 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3202 keyboardFrame.origin.y);
3203 [expectation fulfill];
3208 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3209 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3210 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3211 UIScene* scene = scenes.anyObject;
3212 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3213 UIWindowScene* windowScene = (UIWindowScene*)scene;
3214 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3215 UIWindow* window = windowScene.windows[0];
3216 [window addSubview:viewController.view];
3218 [viewController loadView];
3220 XCTestExpectation* expectation =
3221 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3222 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3223 [NSNotificationCenter.defaultCenter
3224 postNotificationName:UIKeyboardWillShowNotification
3226 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3230 [textInputPlugin handleMethodCall:initialMoveCall
3231 result:^(id _Nullable result){
3236 [textInputPlugin handleMethodCall:subsequentMoveCall
3237 result:^(id _Nullable result){
3244 handleMethodCall:pointerUpCall
3245 result:^(id _Nullable result) {
3246 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3247 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3248 [expectation fulfill];
3252 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3253 [UIView setAnimationsEnabled:YES];
3254 [textInputPlugin showKeyboardAndRemoveScreenshot];
3256 UIView.areAnimationsEnabled,
3257 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3260 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3261 [UIView setAnimationsEnabled:YES];
3262 [textInputPlugin showKeyboardAndRemoveScreenshot];
3264 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3266 return UIView.areAnimationsEnabled;
3268 XCTNSPredicateExpectation* expectation =
3269 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3270 [
self waitForExpectations:@[ expectation ] timeout:10.0];