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;
33 - (void)handleSearchWebAction;
34 - (void)handleLookUpAction;
35 - (void)handleShareAction;
43 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target;
50 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
52 self.receivedNotificationTarget = target;
55 - (BOOL)accessibilityElementIsFocused {
56 return _isAccessibilityFocused;
62 @property(nonatomic, strong) UITextField*
textField;
67 @property(nonatomic, readonly) UIView* inputHider;
68 @property(nonatomic, readonly) UIView* keyboardViewContainer;
69 @property(nonatomic, readonly) UIView* keyboardView;
70 @property(nonatomic, assign) UIView* cachedFirstResponder;
71 @property(nonatomic, readonly) CGRect keyboardRect;
72 @property(nonatomic, readonly) BOOL pendingAutofillRemoval;
73 @property(nonatomic, readonly) BOOL pendingInputViewRemoval;
74 @property(nonatomic, readonly)
75 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
77 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
78 clearText:(BOOL)clearText
79 delayRemoval:(BOOL)delayRemoval;
80 - (NSArray<UIView*>*)textInputViews;
83 - (void)startLiveTextInput;
84 - (void)showKeyboardAndRemoveScreenshot;
90 class MockPlatformViewDelegate :
public PlatformView::Delegate {
92 void OnPlatformViewCreated(std::unique_ptr<Surface> surface)
override {}
93 void OnPlatformViewDestroyed()
override {}
94 void OnPlatformViewScheduleFrame()
override {}
95 void OnPlatformViewAddView(int64_t view_id,
96 const ViewportMetrics& viewport_metrics,
97 AddViewCallback callback)
override {}
98 void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback)
override {}
99 void OnPlatformViewSendViewFocusEvent(
const ViewFocusEvent& event)
override {};
100 void OnPlatformViewSetNextFrameCallback(
const fml::closure& closure)
override {}
101 void OnPlatformViewSetViewportMetrics(int64_t view_id,
const ViewportMetrics& metrics)
override {}
102 const flutter::Settings& OnPlatformViewGetSettings()
const override {
return settings_; }
103 void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage> message)
override {}
104 void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet)
override {
106 void OnPlatformViewDispatchSemanticsAction(int64_t view_id,
108 SemanticsAction action,
109 fml::MallocMapping args)
override {}
110 void OnPlatformViewSetSemanticsEnabled(
bool enabled)
override {}
111 void OnPlatformViewSetAccessibilityFeatures(int32_t flags)
override {}
112 void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture)
override {}
113 void OnPlatformViewUnregisterTexture(int64_t
texture_id)
override {}
114 void OnPlatformViewMarkTextureFrameAvailable(int64_t
texture_id)
override {}
116 void LoadDartDeferredLibrary(intptr_t loading_unit_id,
117 std::unique_ptr<const fml::Mapping> snapshot_data,
118 std::unique_ptr<const fml::Mapping> snapshot_instructions)
override {
120 void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
121 const std::string error_message,
122 bool transient)
override {}
123 void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
124 flutter::AssetResolver::AssetResolverType type)
override {}
136 NSDictionary* _template;
154 UIPasteboard.generalPasteboard.items = @[];
160 [textInputPlugin.autofillContext removeAllObjects];
161 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
162 [[[[textInputPlugin textInputView] superview] subviews]
163 makeObjectsPerformSelector:@selector(removeFromSuperview)];
168 - (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
171 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
172 [textInputPlugin handleMethodCall:setClientCall
173 result:^(id _Nullable result){
177 - (void)setClientClear {
180 [textInputPlugin handleMethodCall:clearClientCall
181 result:^(id _Nullable result){
185 - (void)setTextInputShow {
188 [textInputPlugin handleMethodCall:setClientCall
189 result:^(id _Nullable result){
193 - (void)setTextInputHide {
196 [textInputPlugin handleMethodCall:setClientCall
197 result:^(id _Nullable result){
201 - (void)flushScheduledAsyncBlocks {
202 __block
bool done =
false;
203 XCTestExpectation* expectation =
204 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
205 dispatch_async(dispatch_get_main_queue(), ^{
208 dispatch_async(dispatch_get_main_queue(), ^{
210 [expectation fulfill];
212 [
self waitForExpectations:@[ expectation ] timeout:10];
215 - (NSMutableDictionary*)mutableTemplateCopy {
218 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
219 @"keyboardAppearance" :
@"Brightness.light",
220 @"obscureText" : @NO,
221 @"inputAction" :
@"TextInputAction.unspecified",
222 @"smartDashesType" :
@"0",
223 @"smartQuotesType" :
@"0",
224 @"autocorrect" : @YES,
225 @"enableInteractiveSelection" : @YES,
229 return [_template mutableCopy];
233 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
234 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
238 - (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
239 atIndex:(NSInteger)index {
242 withGranularity:UITextGranularityLine
243 inDirection:UITextLayoutDirectionRight];
248 - (void)updateConfig:(NSDictionary*)config {
251 [textInputPlugin handleMethodCall:updateConfigCall
252 result:^(id _Nullable result){
258 - (void)testWillNotCrashWhenViewControllerIsNil {
265 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
268 result:^(id _Nullable result) {
269 XCTAssertNil(result);
270 [expectation fulfill];
272 XCTAssertNil(inputPlugin.activeView);
273 [
self waitForExpectations:@[ expectation ] timeout:1.0];
276 - (void)testInvokeStartLiveTextInput {
281 result:^(id _Nullable result){
283 OCMVerify([mockPlugin startLiveTextInput]);
286 - (void)testNoDanglingEnginePointer {
296 weakFlutterEngine = flutterEngine;
297 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
299 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
300 weakFlutterTextInputPlugin = flutterTextInputPlugin;
304 NSDictionary* config =
self.mutableTemplateCopy;
307 arguments:@[ [NSNumber numberWithInt:123], config ]];
309 result:^(id _Nullable result){
311 currentView = flutterTextInputPlugin.activeView;
314 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
315 XCTAssertNotNil(currentView,
@"current view must not be nil");
317 XCTAssertNil(weakFlutterTextInputPlugin);
320 XCTAssertNil(currentView.textInputDelegate);
323 - (void)testSecureInput {
324 NSDictionary* config =
self.mutableTemplateCopy;
325 [config setValue:@"YES" forKey:@"obscureText"];
326 [
self setClientId:123 configuration:config];
329 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
336 XCTAssertTrue(inputView.secureTextEntry);
339 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
342 XCTAssertEqual(inputFields.count, 1ul);
350 XCTAssert(inputView.autofillId.length > 0);
353 - (void)testKeyboardType {
354 NSDictionary* config =
self.mutableTemplateCopy;
355 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
356 [
self setClientId:123 configuration:config];
359 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
364 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
367 - (void)testKeyboardTypeWebSearch {
368 NSDictionary* config =
self.mutableTemplateCopy;
369 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
370 [
self setClientId:123 configuration:config];
373 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
378 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
381 - (void)testKeyboardTypeTwitter {
382 NSDictionary* config =
self.mutableTemplateCopy;
383 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
384 [
self setClientId:123 configuration:config];
387 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
392 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
395 - (void)testVisiblePasswordUseAlphanumeric {
396 NSDictionary* config =
self.mutableTemplateCopy;
397 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
398 [
self setClientId:123 configuration:config];
401 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
406 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
409 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
410 NSDictionary* config =
self.mutableTemplateCopy;
411 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
412 [
self setClientId:123 configuration:config];
417 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
418 [
self setClientId:124 configuration:config];
423 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
427 if (@available(iOS 17.0, *)) {
429 OCMVerify(never(), [
engine flutterTextInputView:inputView
430 showAutocorrectionPromptRectForStart:0
434 OCMVerify([
engine flutterTextInputView:inputView
435 showAutocorrectionPromptRectForStart:0
441 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
443 __block
int updateCount = 0;
444 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
445 .andDo(^(NSInvocation* invocation) {
449 [inputView.text setString:@"Some initial text"];
450 XCTAssertEqual(updateCount, 0);
453 [inputView setSelectedTextRange:textRange];
454 XCTAssertEqual(updateCount, 1);
457 NSDictionary* config =
self.mutableTemplateCopy;
458 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
459 [config setValue:@(NO) forKey:@"obscureText"];
460 [config setValue:@(NO) forKey:@"enableDeltaModel"];
461 [inputView configureWithDictionary:config];
464 [inputView setSelectedTextRange:textRange];
466 XCTAssertEqual(updateCount, 1);
469 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
471 if (@available(iOS 17.0, *)) {
475 if (@available(iOS 14.0, *)) {
478 __block
int callCount = 0;
479 OCMStub([
engine flutterTextInputView:inputView
480 showAutocorrectionPromptRectForStart:0
483 .andDo(^(NSInvocation* invocation) {
489 XCTAssertEqual(callCount, 1);
491 UIScribbleInteraction* scribbleInteraction =
492 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
494 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
498 XCTAssertEqual(callCount, 1);
500 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
501 [inputView resetScribbleInteractionStatusIfEnding];
504 XCTAssertEqual(callCount, 2);
506 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
510 XCTAssertEqual(callCount, 2);
512 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
516 XCTAssertEqual(callCount, 2);
518 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
521 XCTAssertEqual(callCount, 3);
525 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
531 arguments:@[ @(123),
self.mutableTemplateCopy ]];
533 result:^(id _Nullable result){
540 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
544 arguments:@{@"transform" : yOffsetMatrix}];
546 result:^(id _Nullable result){
549 if (@available(iOS 17, *)) {
550 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
551 @"The input hider should overlap with the text on and after iOS 17");
554 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
555 @"The input hider should be on the origin of screen on and before iOS 16.");
559 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
565 toPosition:toPosition];
566 NSRange range = flutterRange.
range;
568 XCTAssertEqual(range.location, 0ul);
569 XCTAssertEqual(range.length, 2ul);
572 - (void)testTextInRange {
573 NSDictionary* config =
self.mutableTemplateCopy;
574 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
575 [
self setClientId:123 configuration:config];
576 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
579 [inputView insertText:@"test"];
582 NSString* substring = [inputView textInRange:range];
583 XCTAssertEqual(substring.length, 4ul);
586 substring = [inputView textInRange:range];
587 XCTAssertEqual(substring.length, 0ul);
590 - (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
591 NSDictionary* config =
self.mutableTemplateCopy;
592 [
self setClientId:123 configuration:config];
593 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
596 [inputView insertText:@"text"];
599 NSString* substring = [inputView textInRange:range];
600 XCTAssertNil(substring);
603 - (void)testStandardEditActions {
604 NSDictionary* config =
self.mutableTemplateCopy;
605 [
self setClientId:123 configuration:config];
606 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
609 [inputView insertText:@"aaaa"];
610 [inputView selectAll:nil];
612 [inputView insertText:@"bbbb"];
613 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
614 [inputView paste:nil];
615 [inputView selectAll:nil];
616 [inputView copy:nil];
617 [inputView paste:nil];
618 [inputView selectAll:nil];
619 [inputView delete:nil];
620 [inputView paste:nil];
621 [inputView paste:nil];
624 NSString* substring = [inputView textInRange:range];
625 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
628 - (void)testCanPerformActionForSelectActions {
629 NSDictionary* config =
self.mutableTemplateCopy;
630 [
self setClientId:123 configuration:config];
631 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
634 XCTAssertFalse([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
636 [inputView insertText:@"aaaa"];
638 XCTAssertTrue([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
641 - (void)testCanPerformActionCaptureTextFromCamera {
642 if (@available(iOS 15.0, *)) {
643 NSDictionary* config =
self.mutableTemplateCopy;
644 [
self setClientId:123 configuration:config];
645 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
648 [inputView becomeFirstResponder];
649 XCTAssertTrue([inputView canPerformAction:
@selector(captureTextFromCamera:) withSender:nil]);
651 [inputView insertText:@"test"];
652 [inputView selectAll:nil];
653 XCTAssertTrue([inputView canPerformAction:
@selector(captureTextFromCamera:) withSender:nil]);
657 - (void)testDeletingBackward {
658 NSDictionary* config =
self.mutableTemplateCopy;
659 [
self setClientId:123 configuration:config];
660 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
663 [inputView insertText:@"� ��� ��� text � ���� ���� ��� ���� ��� ���� ��� ���� ���� ���� ��� �� "];
664 [inputView deleteBackward];
665 [inputView deleteBackward];
668 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
669 [inputView deleteBackward];
670 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
671 [inputView deleteBackward];
672 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
673 [inputView deleteBackward];
674 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
675 [inputView deleteBackward];
677 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
678 [inputView deleteBackward];
679 [inputView deleteBackward];
680 [inputView deleteBackward];
681 [inputView deleteBackward];
682 [inputView deleteBackward];
683 [inputView deleteBackward];
685 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
686 [inputView deleteBackward];
687 XCTAssertEqualObjects(inputView.text,
@"ឹ");
688 [inputView deleteBackward];
689 XCTAssertEqualObjects(inputView.text,
@"");
694 - (void)testSystemOnlyAddingPartialComposedCharacter {
695 NSDictionary* config =
self.mutableTemplateCopy;
696 [
self setClientId:123 configuration:config];
697 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
700 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
701 [inputView deleteBackward];
704 [inputView insertText:[@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)]];
705 [inputView insertText:@"� ��"];
707 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
710 [inputView deleteBackward];
713 [inputView insertText:@"� ���"];
714 [inputView deleteBackward];
716 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
717 [inputView insertText:@"� ��"];
718 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
721 [inputView deleteBackward];
724 [inputView deleteBackward];
726 [inputView insertText:[@"� ���" substringWithRange:NSMakeRange(0, 1)]];
727 [inputView insertText:@"� ��"];
729 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
732 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
733 NSDictionary* config =
self.mutableTemplateCopy;
734 [
self setClientId:123 configuration:config];
735 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
738 [inputView insertText:@"� ���� ��� ���� ��� ���� ��� ���"];
739 [inputView deleteBackward];
740 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
743 NSString* brokenEmoji = [@"� ���� ��� ���� ��� ���� ��� ���" substringWithRange:NSMakeRange(0, 1)];
744 [inputView insertText:brokenEmoji];
745 [inputView insertText:@"� ��"];
747 NSString* finalText = [NSString stringWithFormat:@"%@� ��", brokenEmoji];
748 XCTAssertEqualObjects(inputView.text, finalText);
751 - (void)testPastingNonTextDisallowed {
752 NSDictionary* config =
self.mutableTemplateCopy;
753 [
self setClientId:123 configuration:config];
754 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
757 UIPasteboard.generalPasteboard.color = UIColor.redColor;
758 XCTAssertNil(UIPasteboard.generalPasteboard.string);
759 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
760 [inputView paste:nil];
762 XCTAssertEqualObjects(inputView.text,
@"");
765 - (void)testNoZombies {
772 [passwordView.textField description];
774 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
777 - (void)testInputViewCrash {
782 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
783 activeView = inputPlugin.activeView;
785 [activeView updateEditingState];
788 - (void)testDoNotReuseInputViews {
789 NSDictionary* config =
self.mutableTemplateCopy;
790 [
self setClientId:123 configuration:config];
792 [
self setClientId:456 configuration:config];
794 XCTAssertNotNil(currentView);
799 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
801 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
805 - (void)testPropagatePressEventsToViewController {
807 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
808 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
812 NSDictionary* config =
self.mutableTemplateCopy;
813 [
self setClientId:123 configuration:config];
815 [
self setTextInputShow];
817 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
818 withEvent:OCMClassMock([UIPressesEvent class])];
820 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
821 withEvent:[OCMArg isNotNil]]);
822 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
823 withEvent:[OCMArg isNotNil]]);
825 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
826 withEvent:OCMClassMock([UIPressesEvent class])];
828 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
829 withEvent:[OCMArg isNotNil]]);
830 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
831 withEvent:[OCMArg isNotNil]]);
834 - (void)testPropagatePressEventsToViewController2 {
836 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
837 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
841 NSDictionary* config =
self.mutableTemplateCopy;
842 [
self setClientId:123 configuration:config];
843 [
self setTextInputShow];
846 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
847 withEvent:OCMClassMock([UIPressesEvent class])];
849 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
850 withEvent:[OCMArg isNotNil]]);
851 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
852 withEvent:[OCMArg isNotNil]]);
855 [
self setClientId:321 configuration:config];
856 [
self setTextInputShow];
858 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
860 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
861 withEvent:OCMClassMock([UIPressesEvent class])];
863 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
864 withEvent:[OCMArg isNotNil]]);
865 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
866 withEvent:[OCMArg isNotNil]]);
869 - (void)testHotRestart {
870 flutter::MockPlatformViewDelegate mock_platform_view_delegate;
871 auto thread = std::make_unique<fml::Thread>(
"TextInputHotRestart");
872 auto thread_task_runner = thread->GetTaskRunner();
873 flutter::TaskRunners runners(
self.name.UTF8String,
878 id mockFlutterView = OCMClassMock([
FlutterView class]);
881 OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
882 OCMStub([mockFlutterViewController
textInputPlugin]).andReturn(mockFlutterTextInputPlugin);
884 fml::AutoResetWaitableEvent latch;
885 thread_task_runner->PostTask([&] {
886 auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
887 mock_platform_view_delegate,
888 mock_platform_view_delegate.settings_.enable_impeller
894 std::make_shared<fml::SyncSwitch>());
896 platform_view->SetOwnerViewController(mockFlutterViewController);
898 OCMExpect([mockFlutterTextInputPlugin reset]);
900 OCMVerifyAll(mockFlutterView);
907 - (void)testUpdateSecureTextEntry {
908 NSDictionary* config =
self.mutableTemplateCopy;
909 [config setValue:@"YES" forKey:@"obscureText"];
910 [
self setClientId:123 configuration:config];
912 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
915 __block
int callCount = 0;
916 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
920 XCTAssertTrue(inputView.isSecureTextEntry);
922 config =
self.mutableTemplateCopy;
923 [config setValue:@"NO" forKey:@"obscureText"];
924 [
self updateConfig:config];
926 XCTAssertEqual(callCount, 1);
927 XCTAssertFalse(inputView.isSecureTextEntry);
930 - (void)testInputActionContinueAction {
946 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
948 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
951 - (void)testDisablingAutocorrectDisablesSpellChecking {
955 NSDictionary* config =
self.mutableTemplateCopy;
956 [inputView configureWithDictionary:config];
958 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
959 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
961 [config setValue:@(NO) forKey:@"autocorrect"];
962 [inputView configureWithDictionary:config];
964 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
965 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
968 - (void)testEnableInlinePredictionFromConfiguration
API_AVAILABLE(ios(17.0)) {
970 NSMutableDictionary* config =
self.mutableTemplateCopy;
973 [inputView configureWithDictionary:config];
974 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
976 [config setValue:@NO forKey:@"enableInlinePrediction"];
977 [inputView configureWithDictionary:config];
978 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
980 [config setValue:@YES forKey:@"enableInlinePrediction"];
981 [inputView configureWithDictionary:config];
982 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeYes);
985 [config removeObjectForKey:@"enableInlinePrediction"];
986 [inputView configureWithDictionary:config];
987 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
990 [config setValue:[NSNull null] forKey:@"enableInlinePrediction"];
991 [inputView configureWithDictionary:config];
992 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
995 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
997 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1011 XCTAssertEqual(inputView.markedTextRange, nil);
1014 - (void)testFlutterTextInputViewIsNotClearWhenKeyboardShowAndHide {
1017 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1018 XCTAssertEqualObjects(inputView.text,
@"test text");
1021 [
self setTextInputShow];
1022 XCTAssertEqualObjects(inputView.text,
@"test text");
1025 [
self setTextInputHide];
1026 XCTAssertEqualObjects(inputView.text,
@"test text");
1029 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
1033 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
1034 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
1035 if (@available(iOS 17, *)) {
1036 XCTAssertFalse(respondsToInsertionPointColor);
1038 XCTAssertTrue(respondsToInsertionPointColor);
1042 - (void)testSetAttributedMarkedTextSelectedRange
API_AVAILABLE(ios(17.0)) {
1044 NSAttributedString* attributedText =
1045 [[NSAttributedString alloc] initWithString:@"inline prediction"
1047 NSForegroundColorAttributeName : [UIColor grayColor],
1049 [inputView setAttributedMarkedText:attributedText selectedRange:NSMakeRange(0, 7)];
1051 XCTAssertEqualObjects(inputView.text,
@"inline prediction");
1052 NSRange selectedRange = ((
FlutterTextRange*)inputView.selectedTextRange).range;
1053 XCTAssertEqual(selectedRange.location, 0ul);
1054 XCTAssertEqual(selectedRange.length, 7ul);
1056 XCTAssertNotNil(markedRange);
1057 XCTAssertEqual(markedRange.
range.location, 0ul);
1059 XCTAssertEqual(markedRange.
range.length, 17ul);
1062 [inputView setAttributedMarkedText:nil selectedRange:NSMakeRange(0, 0)];
1063 XCTAssertEqualObjects(inputView.text,
@"");
1064 XCTAssertNil(inputView.markedTextRange);
1067 #pragma mark - TextEditingDelta tests
1068 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
1070 inputView.enableDeltaModel = YES;
1072 __block
int updateCount = 0;
1074 [inputView insertText:@"text to insert"];
1077 flutterTextInputView:inputView
1078 updateEditingClient:0
1079 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1080 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1081 isEqualToString:
@""]) &&
1082 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1083 isEqualToString:
@"text to insert"]) &&
1084 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1085 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
1087 .andDo(^(NSInvocation* invocation) {
1090 XCTAssertEqual(updateCount, 0);
1092 [
self flushScheduledAsyncBlocks];
1095 XCTAssertEqual(updateCount, 1);
1097 [inputView deleteBackward];
1098 OCMExpect([
engine flutterTextInputView:inputView
1099 updateEditingClient:0
1100 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1101 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1102 isEqualToString:
@"text to insert"]) &&
1103 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1104 isEqualToString:
@""]) &&
1105 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1107 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1110 .andDo(^(NSInvocation* invocation) {
1113 [
self flushScheduledAsyncBlocks];
1114 XCTAssertEqual(updateCount, 2);
1117 OCMExpect([
engine flutterTextInputView:inputView
1118 updateEditingClient:0
1119 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1120 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1121 isEqualToString:
@"text to inser"]) &&
1122 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1123 isEqualToString:
@""]) &&
1124 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1126 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1129 .andDo(^(NSInvocation* invocation) {
1132 [
self flushScheduledAsyncBlocks];
1133 XCTAssertEqual(updateCount, 3);
1136 withText:@"replace text"];
1139 flutterTextInputView:inputView
1140 updateEditingClient:0
1141 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1142 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1143 isEqualToString:
@"text to inser"]) &&
1144 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1145 isEqualToString:
@"replace text"]) &&
1146 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1147 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
1149 .andDo(^(NSInvocation* invocation) {
1152 [
self flushScheduledAsyncBlocks];
1153 XCTAssertEqual(updateCount, 4);
1155 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1156 OCMExpect([
engine flutterTextInputView:inputView
1157 updateEditingClient:0
1158 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1159 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1160 isEqualToString:
@"replace textext to inser"]) &&
1161 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1162 isEqualToString:
@"marked text"]) &&
1163 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1165 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1168 .andDo(^(NSInvocation* invocation) {
1171 [
self flushScheduledAsyncBlocks];
1172 XCTAssertEqual(updateCount, 5);
1174 [inputView unmarkText];
1176 flutterTextInputView:inputView
1177 updateEditingClient:0
1178 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1179 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1180 isEqualToString:
@"replace textmarked textext to inser"]) &&
1181 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1182 isEqualToString:
@""]) &&
1183 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1185 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1188 .andDo(^(NSInvocation* invocation) {
1191 [
self flushScheduledAsyncBlocks];
1193 XCTAssertEqual(updateCount, 6);
1197 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1200 inputView.enableDeltaModel = YES;
1203 OCMExpect([
engine flutterTextInputView:inputView
1204 updateEditingClient:0
1205 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1206 NSArray* deltas = state[@"deltas"];
1207 NSDictionary* firstDelta = deltas[0];
1208 NSDictionary* secondDelta = deltas[1];
1209 NSDictionary* thirdDelta = deltas[2];
1210 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1211 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1212 [firstDelta[@"deltaStart"] intValue] == 0 &&
1213 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1214 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1215 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1216 [secondDelta[@"deltaStart"] intValue] == 0 &&
1217 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1218 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1219 [thirdDelta[@"deltaText"] isEqualToString:@"� ��"] &&
1220 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1221 [thirdDelta[@"deltaEnd"] intValue] == 0;
1225 [inputView insertText:@"-"];
1226 [inputView deleteBackward];
1227 [inputView insertText:@"� ��"];
1229 [
self flushScheduledAsyncBlocks];
1233 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1235 inputView.enableDeltaModel = YES;
1237 __block
int updateCount = 0;
1238 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1239 .andDo(^(NSInvocation* invocation) {
1243 [inputView.text setString:@"Some initial text"];
1244 XCTAssertEqual(updateCount, 0);
1247 inputView.markedTextRange = range;
1248 inputView.selectedTextRange = nil;
1249 [
self flushScheduledAsyncBlocks];
1250 XCTAssertEqual(updateCount, 1);
1252 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1254 flutterTextInputView:inputView
1255 updateEditingClient:0
1256 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1257 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1258 isEqualToString:
@"Some initial text"]) &&
1259 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1260 isEqualToString:
@"new marked text."]) &&
1261 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1262 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1264 [
self flushScheduledAsyncBlocks];
1265 XCTAssertEqual(updateCount, 2);
1268 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1270 inputView.enableDeltaModel = YES;
1272 __block
int updateCount = 0;
1273 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1274 .andDo(^(NSInvocation* invocation) {
1278 [inputView.text setString:@"Some initial text"];
1279 [
self flushScheduledAsyncBlocks];
1280 XCTAssertEqual(updateCount, 0);
1283 inputView.markedTextRange = range;
1284 inputView.selectedTextRange = nil;
1285 [
self flushScheduledAsyncBlocks];
1286 XCTAssertEqual(updateCount, 1);
1288 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1290 flutterTextInputView:inputView
1291 updateEditingClient:0
1292 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1293 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1294 isEqualToString:
@"Some initial text"]) &&
1295 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1296 isEqualToString:
@"text."]) &&
1297 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1298 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1300 [
self flushScheduledAsyncBlocks];
1301 XCTAssertEqual(updateCount, 2);
1304 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1306 inputView.enableDeltaModel = YES;
1308 __block
int updateCount = 0;
1309 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1310 .andDo(^(NSInvocation* invocation) {
1314 [inputView.text setString:@"Some initial text"];
1315 [
self flushScheduledAsyncBlocks];
1316 XCTAssertEqual(updateCount, 0);
1319 inputView.markedTextRange = range;
1320 inputView.selectedTextRange = nil;
1321 [
self flushScheduledAsyncBlocks];
1322 XCTAssertEqual(updateCount, 1);
1324 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1326 flutterTextInputView:inputView
1327 updateEditingClient:0
1328 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1329 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1330 isEqualToString:
@"Some initial text"]) &&
1331 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1332 isEqualToString:
@"tex"]) &&
1333 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1334 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1336 [
self flushScheduledAsyncBlocks];
1337 XCTAssertEqual(updateCount, 2);
1340 #pragma mark - EditingState tests
1342 - (void)testUITextInputCallsUpdateEditingStateOnce {
1345 __block
int updateCount = 0;
1346 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1347 .andDo(^(NSInvocation* invocation) {
1351 [inputView insertText:@"text to insert"];
1353 XCTAssertEqual(updateCount, 1);
1355 [inputView deleteBackward];
1356 XCTAssertEqual(updateCount, 2);
1359 XCTAssertEqual(updateCount, 3);
1362 withText:@"replace text"];
1363 XCTAssertEqual(updateCount, 4);
1365 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1366 XCTAssertEqual(updateCount, 5);
1368 [inputView unmarkText];
1369 XCTAssertEqual(updateCount, 6);
1372 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1374 inputView.enableDeltaModel = YES;
1376 __block
int updateCount = 0;
1377 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1378 .andDo(^(NSInvocation* invocation) {
1382 [inputView insertText:@"text to insert"];
1383 [
self flushScheduledAsyncBlocks];
1385 XCTAssertEqual(updateCount, 1);
1387 [inputView deleteBackward];
1388 [
self flushScheduledAsyncBlocks];
1389 XCTAssertEqual(updateCount, 2);
1392 [
self flushScheduledAsyncBlocks];
1393 XCTAssertEqual(updateCount, 3);
1396 withText:@"replace text"];
1397 [
self flushScheduledAsyncBlocks];
1398 XCTAssertEqual(updateCount, 4);
1400 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1401 [
self flushScheduledAsyncBlocks];
1402 XCTAssertEqual(updateCount, 5);
1404 [inputView unmarkText];
1405 [
self flushScheduledAsyncBlocks];
1406 XCTAssertEqual(updateCount, 6);
1409 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1412 __block
int updateCount = 0;
1413 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1414 .andDo(^(NSInvocation* invocation) {
1418 [inputView.text setString:@"BEFORE"];
1419 XCTAssertEqual(updateCount, 0);
1421 inputView.markedTextRange = nil;
1422 inputView.selectedTextRange = nil;
1423 XCTAssertEqual(updateCount, 1);
1426 XCTAssertEqual(updateCount, 1);
1427 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1428 XCTAssertEqual(updateCount, 1);
1429 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1430 XCTAssertEqual(updateCount, 1);
1434 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1435 XCTAssertEqual(updateCount, 1);
1437 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1438 XCTAssertEqual(updateCount, 1);
1442 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1443 XCTAssertEqual(updateCount, 1);
1445 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1446 XCTAssertEqual(updateCount, 1);
1449 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1451 inputView.enableDeltaModel = YES;
1453 __block
int updateCount = 0;
1454 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1455 .andDo(^(NSInvocation* invocation) {
1459 [inputView.text setString:@"BEFORE"];
1460 [
self flushScheduledAsyncBlocks];
1461 XCTAssertEqual(updateCount, 0);
1463 inputView.markedTextRange = nil;
1464 inputView.selectedTextRange = nil;
1465 [
self flushScheduledAsyncBlocks];
1466 XCTAssertEqual(updateCount, 1);
1469 XCTAssertEqual(updateCount, 1);
1470 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1471 [
self flushScheduledAsyncBlocks];
1472 XCTAssertEqual(updateCount, 1);
1474 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1475 [
self flushScheduledAsyncBlocks];
1476 XCTAssertEqual(updateCount, 1);
1480 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1481 [
self flushScheduledAsyncBlocks];
1482 XCTAssertEqual(updateCount, 1);
1485 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1486 [
self flushScheduledAsyncBlocks];
1487 XCTAssertEqual(updateCount, 1);
1491 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1492 [
self flushScheduledAsyncBlocks];
1493 XCTAssertEqual(updateCount, 1);
1496 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1497 [
self flushScheduledAsyncBlocks];
1498 XCTAssertEqual(updateCount, 1);
1501 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1504 __block
int updateCount = 0;
1505 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1506 .andDo(^(NSInvocation* invocation) {
1510 [inputView unmarkText];
1512 XCTAssertEqual(updateCount, 0);
1514 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1516 XCTAssertEqual(updateCount, 1);
1518 [inputView unmarkText];
1520 XCTAssertEqual(updateCount, 2);
1523 - (void)testCanCopyPasteWithScribbleEnabled {
1524 if (@available(iOS 14.0, *)) {
1525 NSDictionary* config =
self.mutableTemplateCopy;
1526 [
self setClientId:123 configuration:config];
1527 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1533 [mockInputView insertText:@"aaaa"];
1534 [mockInputView selectAll:nil];
1536 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1537 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1538 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1539 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1541 [mockInputView copy:NULL];
1542 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1543 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1544 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1545 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1549 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1550 if (@available(iOS 14.0, *)) {
1553 __block
int updateCount = 0;
1554 OCMStub([
engine flutterTextInputView:inputView
1555 updateEditingClient:0
1556 withState:[OCMArg isNotNil]])
1557 .andDo(^(NSInvocation* invocation) {
1561 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1563 XCTAssertEqual(updateCount, 1);
1565 UIScribbleInteraction* scribbleInteraction =
1566 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1568 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1569 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1571 XCTAssertEqual(updateCount, 1);
1573 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1574 [inputView resetScribbleInteractionStatusIfEnding];
1575 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1577 XCTAssertEqual(updateCount, 2);
1579 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1580 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1583 XCTAssertEqual(updateCount, 2);
1585 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1586 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1589 XCTAssertEqual(updateCount, 2);
1591 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1592 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1594 XCTAssertEqual(updateCount, 3);
1598 - (void)testUpdateEditingClientNegativeSelection {
1601 [inputView.text setString:@"SELECTION"];
1602 inputView.markedTextRange = nil;
1603 inputView.selectedTextRange = nil;
1605 [inputView setTextInputState:@{
1606 @"text" : @"SELECTION",
1607 @"selectionBase" : @-1,
1608 @"selectionExtent" : @-1
1610 [inputView updateEditingState];
1611 OCMVerify([
engine flutterTextInputView:inputView
1612 updateEditingClient:0
1613 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1614 return ([state[
@"selectionBase"] intValue]) == 0 &&
1615 ([state[
@"selectionExtent"] intValue] == 0);
1620 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1621 [inputView updateEditingState];
1622 OCMVerify([
engine flutterTextInputView:inputView
1623 updateEditingClient:0
1624 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1625 return ([state[
@"selectionBase"] intValue]) == 0 &&
1626 ([state[
@"selectionExtent"] intValue] == 0);
1630 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1631 [inputView updateEditingState];
1632 OCMVerify([
engine flutterTextInputView:inputView
1633 updateEditingClient:0
1634 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1635 return ([state[
@"selectionBase"] intValue]) == 0 &&
1636 ([state[
@"selectionExtent"] intValue] == 0);
1640 - (void)testUpdateEditingClientSelectionClamping {
1644 [inputView.text setString:@"SELECTION"];
1645 inputView.markedTextRange = nil;
1646 inputView.selectedTextRange = nil;
1649 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1650 [inputView updateEditingState];
1651 OCMVerify([
engine flutterTextInputView:inputView
1652 updateEditingClient:0
1653 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1654 return ([state[
@"selectionBase"] intValue]) == 0 &&
1655 ([state[
@"selectionExtent"] intValue] == 0);
1659 [inputView setTextInputState:@{
1660 @"text" : @"SELECTION",
1661 @"selectionBase" : @0,
1662 @"selectionExtent" : @9999
1664 [inputView updateEditingState];
1666 OCMVerify([
engine flutterTextInputView:inputView
1667 updateEditingClient:0
1668 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1669 return ([state[
@"selectionBase"] intValue]) == 0 &&
1670 ([state[
@"selectionExtent"] intValue] == 9);
1675 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1676 [inputView updateEditingState];
1677 OCMVerify([
engine flutterTextInputView:inputView
1678 updateEditingClient:0
1679 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1680 return ([state[
@"selectionBase"] intValue]) == 0 &&
1681 ([state[
@"selectionExtent"] intValue] == 1);
1685 [inputView setTextInputState:@{
1686 @"text" : @"SELECTION",
1687 @"selectionBase" : @9999,
1688 @"selectionExtent" : @9999
1690 [inputView updateEditingState];
1691 OCMVerify([
engine flutterTextInputView:inputView
1692 updateEditingClient:0
1693 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1694 return ([state[
@"selectionBase"] intValue]) == 9 &&
1695 ([state[
@"selectionExtent"] intValue] == 9);
1699 - (void)testInputViewsHasNonNilInputDelegate {
1701 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1703 [inputView setTextInputClient:123];
1704 [inputView reloadInputViews];
1705 [inputView becomeFirstResponder];
1706 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1707 inputView.inputDelegate = nil;
1711 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1712 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1713 [inputView removeFromSuperview];
1716 - (void)testInputViewsDoNotHaveUITextInteractions {
1718 BOOL hasTextInteraction = NO;
1719 for (
id interaction in inputView.interactions) {
1720 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1721 if (hasTextInteraction) {
1725 XCTAssertFalse(hasTextInteraction);
1728 #pragma mark - UITextInput methods - Tests
1730 - (void)testUpdateFirstRectForRange {
1731 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1737 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1742 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1743 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1747 NSArray* affineMatrix = @[
1748 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1749 @(-6.0), @(3.0), @(9.0), @(1.0)
1753 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1755 [inputView setEditableTransform:yOffsetMatrix];
1757 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1760 CGRect testRect = CGRectMake(0, 0, 100, 100);
1761 [inputView setMarkedRect:testRect];
1763 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1764 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1766 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1769 [inputView setEditableTransform:zeroMatrix];
1771 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1772 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1775 [inputView setEditableTransform:yOffsetMatrix];
1776 [inputView setMarkedRect:testRect];
1777 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1780 [inputView setMarkedRect:kInvalidFirstRect];
1782 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1783 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1786 [inputView setEditableTransform:affineMatrix];
1787 [inputView setMarkedRect:testRect];
1789 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1791 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1792 const CGPoint offset = CGPointMake(113, 119);
1793 CGRect currentFrame = inputView.frame;
1794 currentFrame.origin = offset;
1795 inputView.frame = currentFrame;
1798 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1799 [inputView firstRectForRange:range]));
1802 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1804 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1809 [inputView setSelectionRects:@[
1818 if (@available(iOS 17, *)) {
1819 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1820 [inputView firstRectForRange:multiRectRange]));
1822 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1823 [inputView firstRectForRange:multiRectRange]));
1827 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1829 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1831 [inputView setSelectionRects:@[
1838 if (@available(iOS 17, *)) {
1839 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1840 [inputView firstRectForRange:singleRectRange]));
1842 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1847 if (@available(iOS 17, *)) {
1848 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1849 [inputView firstRectForRange:multiRectRange]));
1851 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1854 [inputView setTextInputState:@{@"text" : @"COM"}];
1856 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1859 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1861 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1863 [inputView setSelectionRects:@[
1870 if (@available(iOS 17, *)) {
1871 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1872 [inputView firstRectForRange:singleRectRange]));
1874 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1878 if (@available(iOS 17, *)) {
1879 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1880 [inputView firstRectForRange:multiRectRange]));
1882 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1885 [inputView setTextInputState:@{@"text" : @"COM"}];
1887 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1890 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1892 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1894 [inputView setSelectionRects:@[
1905 if (@available(iOS 17, *)) {
1906 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1907 [inputView firstRectForRange:singleRectRange]));
1909 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1914 if (@available(iOS 17, *)) {
1915 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1916 [inputView firstRectForRange:multiRectRange]));
1918 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1922 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1924 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1926 [inputView setSelectionRects:@[
1937 if (@available(iOS 17, *)) {
1938 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1939 [inputView firstRectForRange:singleRectRange]));
1941 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1945 if (@available(iOS 17, *)) {
1946 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1947 [inputView firstRectForRange:multiRectRange]));
1949 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1953 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1955 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1957 [inputView setSelectionRects:@[
1968 if (@available(iOS 17, *)) {
1969 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1970 [inputView firstRectForRange:multiRectRange]));
1972 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1976 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1978 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1980 [inputView setSelectionRects:@[
1991 if (@available(iOS 17, *)) {
1992 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1993 [inputView firstRectForRange:multiRectRange]));
1995 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1999 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
2001 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2003 [inputView setSelectionRects:@[
2014 if (@available(iOS 17, *)) {
2015 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
2016 [inputView firstRectForRange:multiRectRange]));
2018 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2022 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
2024 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2026 [inputView setSelectionRects:@[
2037 if (@available(iOS 17, *)) {
2038 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
2039 [inputView firstRectForRange:multiRectRange]));
2041 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2045 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
2047 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2049 [inputView setSelectionRects:@[
2060 if (@available(iOS 17, *)) {
2061 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
2062 [inputView firstRectForRange:multiRectRange]));
2064 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2068 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
2070 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2072 [inputView setSelectionRects:@[
2083 if (@available(iOS 17, *)) {
2084 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
2085 [inputView firstRectForRange:multiRectRange]));
2087 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2091 - (void)testClosestPositionToPoint {
2093 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2096 [inputView setSelectionRects:@[
2101 CGPoint point = CGPointMake(150, 150);
2102 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2103 XCTAssertEqual(UITextStorageDirectionBackward,
2108 [inputView setSelectionRects:@[
2115 point = CGPointMake(125, 150);
2116 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2117 XCTAssertEqual(UITextStorageDirectionForward,
2122 [inputView setSelectionRects:@[
2129 point = CGPointMake(125, 201);
2130 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2131 XCTAssertEqual(UITextStorageDirectionBackward,
2135 [inputView setSelectionRects:@[
2141 point = CGPointMake(125, 250);
2142 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2143 XCTAssertEqual(UITextStorageDirectionBackward,
2147 [inputView setSelectionRects:@[
2152 point = CGPointMake(110, 50);
2153 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2154 XCTAssertEqual(UITextStorageDirectionForward,
2159 [inputView beginFloatingCursorAtPoint:CGPointZero];
2160 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2161 XCTAssertEqual(UITextStorageDirectionForward,
2163 [inputView endFloatingCursor];
2166 - (void)testClosestPositionToPointRTL {
2168 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2170 [inputView setSelectionRects:@[
2186 XCTAssertEqual(0U, position.
index);
2187 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2189 XCTAssertEqual(1U, position.
index);
2190 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2192 XCTAssertEqual(1U, position.
index);
2193 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2195 XCTAssertEqual(2U, position.
index);
2196 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2198 XCTAssertEqual(2U, position.
index);
2199 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2201 XCTAssertEqual(3U, position.
index);
2202 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2204 XCTAssertEqual(3U, position.
index);
2205 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2208 - (void)testSelectionRectsForRange {
2210 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2212 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2213 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2214 [inputView setSelectionRects:@[
2223 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2224 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2225 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2229 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2230 XCTAssertTrue(CGRectEqualToRect(
2231 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2232 [inputView selectionRectsForRange:range][0].rect));
2235 - (void)testClosestPositionToPointWithinRange {
2237 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2240 [inputView setSelectionRects:@[
2247 CGPoint point = CGPointMake(125, 150);
2250 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2252 UITextStorageDirectionForward,
2253 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2256 [inputView setSelectionRects:@[
2263 point = CGPointMake(125, 150);
2266 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2268 UITextStorageDirectionForward,
2269 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2272 - (void)testClosestPositionToPointWithPartialSelectionRects {
2274 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2281 XCTAssertTrue(CGRectEqualToRect(
2284 affinity:UITextStorageDirectionForward]],
2285 CGRectMake(100, 0, 0, 100)));
2288 XCTAssertTrue(CGRectEqualToRect(
2291 affinity:UITextStorageDirectionForward]],
2295 #pragma mark - Floating Cursor - Tests
2297 - (void)testFloatingCursorDoesNotThrow {
2300 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2301 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2302 [inputView endFloatingCursor];
2303 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2304 [inputView endFloatingCursor];
2307 - (void)testFloatingCursor {
2309 [inputView setTextInputState:@{
2311 @"selectionBase" : @1,
2312 @"selectionExtent" : @1,
2323 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2326 XCTAssertTrue(CGRectEqualToRect(
2329 affinity:UITextStorageDirectionForward]],
2330 CGRectMake(0, 0, 0, 100)));
2333 XCTAssertTrue(CGRectEqualToRect(
2336 affinity:UITextStorageDirectionForward]],
2337 CGRectMake(100, 100, 0, 100)));
2338 XCTAssertTrue(CGRectEqualToRect(
2341 affinity:UITextStorageDirectionForward]],
2342 CGRectMake(200, 200, 0, 100)));
2343 XCTAssertTrue(CGRectEqualToRect(
2346 affinity:UITextStorageDirectionForward]],
2347 CGRectMake(300, 300, 0, 100)));
2350 XCTAssertTrue(CGRectEqualToRect(
2353 affinity:UITextStorageDirectionForward]],
2354 CGRectMake(400, 300, 0, 100)));
2356 XCTAssertTrue(CGRectEqualToRect(
2359 affinity:UITextStorageDirectionForward]],
2363 [inputView setTextInputState:@{
2365 @"selectionBase" : @2,
2366 @"selectionExtent" : @2,
2369 XCTAssertTrue(CGRectEqualToRect(
2372 affinity:UITextStorageDirectionBackward]],
2373 CGRectMake(0, 0, 0, 100)));
2376 XCTAssertTrue(CGRectEqualToRect(
2379 affinity:UITextStorageDirectionBackward]],
2380 CGRectMake(100, 0, 0, 100)));
2381 XCTAssertTrue(CGRectEqualToRect(
2384 affinity:UITextStorageDirectionBackward]],
2385 CGRectMake(200, 100, 0, 100)));
2386 XCTAssertTrue(CGRectEqualToRect(
2389 affinity:UITextStorageDirectionBackward]],
2390 CGRectMake(300, 200, 0, 100)));
2391 XCTAssertTrue(CGRectEqualToRect(
2394 affinity:UITextStorageDirectionBackward]],
2395 CGRectMake(400, 300, 0, 100)));
2397 XCTAssertTrue(CGRectEqualToRect(
2400 affinity:UITextStorageDirectionBackward]],
2405 CGRect initialBounds = inputView.bounds;
2406 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2407 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2408 OCMVerify([
engine flutterTextInputView:inputView
2409 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2411 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2412 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2413 ([state[
@"Y"] isEqualToNumber:@(0)]);
2416 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2417 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2418 OCMVerify([
engine flutterTextInputView:inputView
2419 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2421 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2422 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2423 ([state[
@"Y"] isEqualToNumber:@(333)]);
2426 [inputView endFloatingCursor];
2427 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2428 OCMVerify([
engine flutterTextInputView:inputView
2429 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2431 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2432 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2433 ([state[
@"Y"] isEqualToNumber:@(0)]);
2437 #pragma mark - UIKeyInput Overrides - Tests
2439 - (void)testInsertTextAddsPlaceholderSelectionRects {
2442 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2452 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2455 [inputView insertText:@"in"];
2483 #pragma mark - Autofill - Utilities
2485 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2488 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2489 @"keyboardAppearance" :
@"Brightness.light",
2490 @"obscureText" : @YES,
2491 @"inputAction" :
@"TextInputAction.unspecified",
2492 @"smartDashesType" :
@"0",
2493 @"smartQuotesType" :
@"0",
2494 @"autocorrect" : @YES
2498 return [_passwordTemplate mutableCopy];
2502 return [
self.installedInputViews
2503 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2506 - (void)commitAutofillContextAndVerify {
2510 [textInputPlugin handleMethodCall:methodCall
2511 result:^(id _Nullable result){
2514 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2519 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2523 #pragma mark - Autofill - Tests
2525 - (void)testDisablingAutofillOnInputClient {
2526 NSDictionary* config =
self.mutableTemplateCopy;
2527 [config setValue:@"YES" forKey:@"obscureText"];
2529 [
self setClientId:123 configuration:config];
2532 XCTAssertEqualObjects(inputView.textContentType,
@"");
2535 - (void)testAutofillEnabledByDefault {
2536 NSDictionary* config =
self.mutableTemplateCopy;
2537 [config setValue:@"NO" forKey:@"obscureText"];
2538 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2539 forKey:@"autofill"];
2541 [
self setClientId:123 configuration:config];
2544 XCTAssertNil(inputView.textContentType);
2547 - (void)testAutofillContext {
2548 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2551 @"uniqueIdentifier" : @"field1",
2552 @"hints" : @[ @"hint1" ],
2553 @"editingValue" : @{@"text" : @""}
2555 forKey:@"autofill"];
2557 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2559 @"uniqueIdentifier" : @"field2",
2560 @"hints" : @[ @"hint2" ],
2561 @"editingValue" : @{@"text" : @""}
2563 forKey:@"autofill"];
2565 NSMutableDictionary* config = [field1 mutableCopy];
2566 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2568 [
self setClientId:123 configuration:config];
2569 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2573 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2574 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2576 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2579 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2581 @"uniqueIdentifier" : @"field3",
2582 @"hints" : @[ @"hint3" ],
2583 @"editingValue" : @{@"text" : @""}
2585 forKey:@"autofill"];
2589 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2591 [
self setClientId:123 configuration:config];
2593 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2596 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2597 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2599 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2602 for (NSString* key in oldContext.allKeys) {
2603 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2607 config =
self.mutablePasswordTemplateCopy;
2610 [
self setClientId:124 configuration:config];
2611 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2613 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2616 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2617 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2620 for (NSString* key in oldContext.allKeys) {
2621 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2625 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2629 [
self setClientId:200 configuration:config];
2632 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2635 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2636 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2639 for (NSString* key in oldContext.allKeys) {
2640 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2643 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2646 - (void)testCommitAutofillContext {
2647 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2649 @"uniqueIdentifier" : @"field1",
2650 @"hints" : @[ @"hint1" ],
2651 @"editingValue" : @{@"text" : @""}
2653 forKey:@"autofill"];
2655 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2657 @"uniqueIdentifier" : @"field2",
2658 @"hints" : @[ @"hint2" ],
2659 @"editingValue" : @{@"text" : @""}
2661 forKey:@"autofill"];
2663 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2665 @"uniqueIdentifier" : @"field3",
2666 @"hints" : @[ @"hint3" ],
2667 @"editingValue" : @{@"text" : @""}
2669 forKey:@"autofill"];
2671 NSMutableDictionary* config = [field1 mutableCopy];
2672 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2674 [
self setClientId:123 configuration:config];
2675 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2677 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2679 [
self commitAutofillContextAndVerify];
2680 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2683 [
self setClientId:123 configuration:config];
2685 [
self setClientId:124 configuration:field3];
2686 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2688 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2689 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2692 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2694 [
self commitAutofillContextAndVerify];
2695 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2698 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2700 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2704 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2705 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2707 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2709 [
self commitAutofillContextAndVerify];
2710 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2713 - (void)testAutofillInputViews {
2714 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2716 @"uniqueIdentifier" : @"field1",
2717 @"hints" : @[ @"hint1" ],
2718 @"editingValue" : @{@"text" : @""}
2720 forKey:@"autofill"];
2722 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2724 @"uniqueIdentifier" : @"field2",
2725 @"hints" : @[ @"hint2" ],
2726 @"editingValue" : @{@"text" : @""}
2728 forKey:@"autofill"];
2730 NSMutableDictionary* config = [field1 mutableCopy];
2731 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2733 [
self setClientId:123 configuration:config];
2734 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2737 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2740 XCTAssertEqual(inputFields.count, 2ul);
2741 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2746 withText:@"Autofilled!"];
2747 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2750 OCMVerify([
engine flutterTextInputView:inactiveView
2751 updateEditingClient:0
2752 withState:[OCMArg isNotNil]
2753 withTag:
@"field2"]);
2756 - (void)testAutofillContextPersistsAfterClearClient {
2757 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2759 @"uniqueIdentifier" : @"field1",
2760 @"hints" : @[ @"username" ],
2761 @"editingValue" : @{@"text" : @""}
2763 forKey:@"autofill"];
2765 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2767 @"uniqueIdentifier" : @"field2",
2768 @"hints" : @[ @"password" ],
2769 @"editingValue" : @{@"text" : @""}
2771 forKey:@"autofill"];
2773 NSMutableDictionary* config = [field1 mutableCopy];
2774 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2777 [
self setClientId:123 configuration:config];
2782 [
self setClientClear];
2787 [
self commitAutofillContextAndVerify];
2792 - (void)testSetClientResetsPendingAutofillRemoval {
2796 NSMutableDictionary* field =
self.mutablePasswordTemplateCopy;
2798 @"uniqueIdentifier" : @"field1",
2799 @"hints" : @[ @"password" ],
2800 @"editingValue" : @{@"text" : @""}
2802 forKey:@"autofill"];
2803 [field setValue:@[ field ] forKey:@"fields"];
2806 [
self setClientId:123 configuration:field];
2810 [
self setClientClear];
2814 [
self setClientId:456 configuration:self.mutableTemplateCopy];
2819 - (void)testPendingInputViewRemovalAfterClearClient {
2823 NSDictionary* config =
self.mutableTemplateCopy;
2826 [
self setClientId:123 configuration:config];
2833 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2836 [
self setClientClear];
2841 [
self setTextInputHide];
2846 - (void)testHideBeforeClearClientRemovesViewImmediately {
2850 NSDictionary* config =
self.mutableTemplateCopy;
2852 [
self setClientId:123 configuration:config];
2853 [
self setTextInputShow];
2857 [
self setTextInputHide];
2862 [
self setClientClear];
2867 - (void)testSetClientResetsPendingInputViewRemoval {
2871 NSDictionary* config =
self.mutableTemplateCopy;
2874 [
self setClientId:123 configuration:config];
2875 [
self setTextInputShow];
2880 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2882 [
self setClientClear];
2886 [
self setClientId:456 configuration:config];
2890 - (void)testPasswordAutofillHack {
2891 NSDictionary* config =
self.mutableTemplateCopy;
2892 [config setValue:@"YES" forKey:@"obscureText"];
2893 [
self setClientId:123 configuration:config];
2896 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2900 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2903 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2906 - (void)testClearAutofillContextClearsSelection {
2907 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2908 NSDictionary* editingValue = @{
2909 @"text" :
@"REGULAR_TEXT_FIELD",
2910 @"composingBase" : @0,
2911 @"composingExtent" : @3,
2912 @"selectionBase" : @1,
2913 @"selectionExtent" : @4
2915 [regularField setValue:@{
2916 @"uniqueIdentifier" : @"field2",
2917 @"hints" : @[ @"hint2" ],
2918 @"editingValue" : editingValue,
2920 forKey:@"autofill"];
2921 [regularField addEntriesFromDictionary:editingValue];
2922 [
self setClientId:123 configuration:regularField];
2923 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2924 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2927 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2929 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2933 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2934 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2936 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2938 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2939 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2942 XCTAssert([oldInputView.text isEqualToString:
@""]);
2944 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2947 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2949 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2950 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2952 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2955 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2956 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2958 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2960 [
self commitAutofillContextAndVerify];
2963 - (void)testScribbleSetSelectionRects {
2964 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2965 NSDictionary* editingValue = @{
2966 @"text" :
@"REGULAR_TEXT_FIELD",
2967 @"composingBase" : @0,
2968 @"composingExtent" : @3,
2969 @"selectionBase" : @1,
2970 @"selectionExtent" : @4
2972 [regularField setValue:@{
2973 @"uniqueIdentifier" : @"field1",
2974 @"hints" : @[ @"hint2" ],
2975 @"editingValue" : editingValue,
2977 forKey:@"autofill"];
2978 [regularField addEntriesFromDictionary:editingValue];
2979 [
self setClientId:123 configuration:regularField];
2980 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2981 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2983 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2984 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2988 [textInputPlugin handleMethodCall:methodCall
2989 result:^(id _Nullable result){
2992 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2995 - (void)testDecommissionedViewAreNotReusedByAutofill {
2997 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2998 [configuration setValue:@{
2999 @"uniqueIdentifier" : @"field1",
3000 @"hints" : @[ UITextContentTypePassword ],
3001 @"editingValue" : @{@"text" : @""}
3003 forKey:@"autofill"];
3004 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3006 [
self setClientId:123 configuration:configuration];
3008 [
self setTextInputHide];
3011 [
self setClientId:124 configuration:configuration];
3015 XCTAssertNotNil(previousActiveView);
3019 - (void)testInitialActiveViewCantAccessTextInputDelegate {
3026 - (void)testAutoFillDoesNotTriggerOnShowAndHideKeyboard {
3028 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
3029 [configuration setValue:@{
3030 @"uniqueIdentifier" : @"field1",
3031 @"hints" : @[ UITextContentTypePassword ],
3032 @"editingValue" : @{@"text" : @""}
3034 forKey:@"autofill"];
3035 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3036 [
self setClientId:123 configuration:configuration];
3038 [
self setTextInputShow];
3039 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3042 [
self setTextInputHide];
3043 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3045 [
self commitAutofillContextAndVerify];
3048 #pragma mark - Accessibility - Tests
3050 - (void)testUITextInputAccessibilityNotHiddenWhenKeyboardIsShownAndHidden {
3051 [
self setClientId:123 configuration:self.mutableTemplateCopy];
3054 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
3057 XCTAssertEqual([inputFields count], 1u);
3060 [
self setTextInputShow];
3062 inputFields =
self.installedInputViews;
3064 XCTAssertEqual([inputFields count], 1u);
3067 [
self setTextInputHide];
3069 inputFields =
self.installedInputViews;
3071 XCTAssertEqual([inputFields count], 1u);
3074 [
self setClientClear];
3076 inputFields =
self.installedInputViews;
3079 XCTAssertEqual([inputFields count], 0u);
3082 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
3085 UIView* container = [[UIView alloc] init];
3086 UIAccessibilityElement* backing =
3087 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
3088 inputView.backingTextInputAccessibilityObject = backing;
3091 [inputView accessibilityElementDidBecomeFocused];
3097 - (void)testFlutterTokenizerCanParseLines {
3099 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3102 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3103 XCTAssertEqual(range.
range.location, 0u);
3104 XCTAssertEqual(range.
range.length, 0u);
3106 [inputView insertText:@"how are you\nI am fine, Thank you"];
3108 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3109 XCTAssertEqual(range.
range.location, 0u);
3110 XCTAssertEqual(range.
range.length, 11u);
3112 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
3113 XCTAssertEqual(range.
range.location, 0u);
3114 XCTAssertEqual(range.
range.length, 11u);
3116 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
3117 XCTAssertEqual(range.
range.location, 0u);
3118 XCTAssertEqual(range.
range.length, 11u);
3120 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
3121 XCTAssertEqual(range.
range.location, 12u);
3122 XCTAssertEqual(range.
range.length, 20u);
3124 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
3125 XCTAssertEqual(range.
range.location, 12u);
3126 XCTAssertEqual(range.
range.length, 20u);
3128 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
3129 XCTAssertEqual(range.
range.location, 12u);
3130 XCTAssertEqual(range.
range.length, 20u);
3133 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
3135 [inputView insertText:@"0123456789\n012345"];
3136 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3139 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3140 withGranularity:UITextGranularityLine
3141 inDirection:UITextStorageDirectionBackward];
3142 XCTAssertEqual(range.
range.location, 11u);
3143 XCTAssertEqual(range.
range.length, 6u);
3146 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
3148 [inputView insertText:@"0123456789\n012345"];
3149 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3152 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3153 withGranularity:UITextGranularityLine
3154 inDirection:UITextStorageDirectionForward];
3155 if (@available(iOS 17.0, *)) {
3156 XCTAssertNil(range);
3158 XCTAssertEqual(range.
range.location, 11u);
3159 XCTAssertEqual(range.
range.length, 6u);
3163 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
3165 [inputView insertText:@"0123456789\n012345"];
3166 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3171 withGranularity:UITextGranularityLine
3172 inDirection:UITextStorageDirectionForward];
3173 if (@available(iOS 17.0, *)) {
3174 XCTAssertNil(range);
3176 XCTAssertEqual(range.
range.location, 0u);
3177 XCTAssertEqual(range.
range.length, 0u);
3181 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
3186 __weak UIView* activeView;
3191 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
3194 result:^(id _Nullable result){
3200 result:^(id _Nullable result){
3202 XCTAssertNotNil(activeView);
3205 XCTAssertNotNil(activeView);
3208 - (void)testFlutterTextInputPluginHostViewNilCrash {
3211 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
3214 - (void)testFlutterTextInputPluginHostViewNotNil {
3220 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
3223 - (void)testSetPlatformViewClient {
3230 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
3232 result:^(id _Nullable result){
3235 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
3238 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
3240 result:^(id _Nullable result){
3242 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
3245 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
3246 if (@available(iOS 16.0, *)) {
3248 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3249 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
3250 @"editMenuInteraction setup delegate correctly");
3254 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
3255 if (@available(iOS 16.0, *)) {
3259 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
3263 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
3264 if (@available(iOS 16.0, *)) {
3269 [myViewController loadView];
3272 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3274 result:^(id _Nullable result){
3280 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3282 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3283 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3285 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3286 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3287 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3288 .andDo(^(NSInvocation* invocation) {
3290 [invocation retainArguments];
3291 UIEditMenuConfiguration* config;
3292 [invocation getArgument:&config atIndex:2];
3293 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
3294 @"UIEditMenuConfiguration must use automatic arrow direction.");
3295 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
3296 @"UIEditMenuConfiguration must have the correct point.");
3297 [expectation fulfill];
3300 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3301 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
3303 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3304 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3305 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3309 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
3310 if (@available(iOS 16.0, *)) {
3315 [myViewController loadView];
3319 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3321 result:^(id _Nullable result){
3327 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3329 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3330 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3332 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3333 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3334 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3335 .andDo(^(NSInvocation* invocation) {
3336 [expectation fulfill];
3339 myInputView.frame = CGRectMake(10, 20, 30, 40);
3340 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3341 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3343 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3344 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3345 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3348 [myInputView editMenuInteraction:mockInteraction
3349 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3351 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3352 @"targetRectForConfiguration must return the correct target rect.");
3356 - (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3357 if (@available(iOS 16.0, *)) {
3362 [myViewController loadView];
3366 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3368 result:^(id _Nullable result){
3374 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3376 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3377 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3379 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3380 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3381 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3382 .andDo(^(NSInvocation* invocation) {
3383 [expectation fulfill];
3386 myInputView.frame = CGRectMake(10, 20, 30, 40);
3387 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3388 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3390 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3391 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3392 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3394 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3396 action:@selector(copy:)
3398 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3400 action:@selector(paste:)
3402 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3404 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3405 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3406 suggestedActions:suggestedActions];
3407 XCTAssertEqualObjects(menu.children, suggestedActions,
3408 @"Must show suggested items by default.");
3412 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3413 if (@available(iOS 16.0, *)) {
3418 [myViewController loadView];
3422 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3424 result:^(id _Nullable result){
3430 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3432 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3433 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3435 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3436 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3437 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3438 .andDo(^(NSInvocation* invocation) {
3439 [expectation fulfill];
3442 myInputView.frame = CGRectMake(10, 20, 30, 40);
3443 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3444 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3446 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3447 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3449 BOOL shownEditMenu =
3450 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3451 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3452 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3454 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3456 action:@selector(copy:)
3458 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3460 action:@selector(paste:)
3462 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3464 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3465 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3466 suggestedActions:suggestedActions];
3468 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3469 XCTAssertEqualObjects(menu.children, expectedChildren);
3473 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3474 if (@available(iOS 16.0, *)) {
3479 [myViewController loadView];
3483 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3485 result:^(id _Nullable result){
3491 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3493 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3494 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3496 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3497 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3498 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3499 .andDo(^(NSInvocation* invocation) {
3500 [expectation fulfill];
3503 myInputView.frame = CGRectMake(10, 20, 30, 40);
3504 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3505 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3507 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3508 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3510 BOOL shownEditMenu =
3511 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3512 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3513 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3515 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3517 action:@selector(copy:)
3519 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3521 action:@selector(cut:)
3523 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3525 action:@selector(paste:)
3538 NSArray<UIMenuElement*>* suggestedActions = @[
3539 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3540 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3543 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3544 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3545 suggestedActions:suggestedActions];
3547 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3548 XCTAssertEqualObjects(menu.children, expectedActions);
3552 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3553 if (@available(iOS 16.0, *)) {
3558 [myViewController loadView];
3562 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3564 result:^(id _Nullable result){
3570 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3572 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3573 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3575 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3576 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3577 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3578 .andDo(^(NSInvocation* invocation) {
3579 [expectation fulfill];
3582 myInputView.frame = CGRectMake(10, 20, 30, 40);
3583 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3584 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3586 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3587 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3588 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3591 BOOL shownEditMenu =
3592 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3593 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3594 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3596 NSArray<UICommand*>* suggestedActions = @[
3597 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3600 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3601 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3602 suggestedActions:suggestedActions];
3603 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3605 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3606 @"Must create search web item in the tree.");
3607 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3608 @"Must create look up item in the tree.");
3609 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3610 @"Must create share item in the tree.");
3614 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3615 if (@available(iOS 17.0, *)) {
3616 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3617 "https://github.com/flutter/flutter/issues/183473");
3620 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3622 [inputView setTextInputClient:123];
3623 [inputView reloadInputViews];
3624 [inputView becomeFirstResponder];
3625 XCTAssert(inputView.isFirstResponder);
3627 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3628 [NSNotificationCenter.defaultCenter
3629 postNotificationName:UIKeyboardWillShowNotification
3631 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3635 [textInputPlugin handleMethodCall:onPointerMoveCall
3636 result:^(id _Nullable result){
3638 XCTAssertFalse(inputView.isFirstResponder);
3642 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3643 if (@available(iOS 17.0, *)) {
3644 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3645 "https://github.com/flutter/flutter/issues/183473");
3647 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3648 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3649 UIScene* scene = scenes.anyObject;
3650 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3651 UIWindowScene* windowScene = (UIWindowScene*)scene;
3652 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3653 UIWindow* window = windowScene.windows[0];
3654 [window addSubview:viewController.view];
3656 [viewController loadView];
3659 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3661 [inputView setTextInputClient:123];
3662 [inputView reloadInputViews];
3663 [inputView becomeFirstResponder];
3666 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3667 [subView removeFromSuperview];
3671 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3672 [NSNotificationCenter.defaultCenter
3673 postNotificationName:UIKeyboardWillShowNotification
3675 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3679 [textInputPlugin handleMethodCall:onPointerMoveCall
3680 result:^(id _Nullable result){
3683 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3684 [subView removeFromSuperview];
3689 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3690 if (@available(iOS 17.0, *)) {
3691 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3692 "https://github.com/flutter/flutter/issues/183473");
3694 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3695 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3696 UIScene* scene = scenes.anyObject;
3697 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3698 UIWindowScene* windowScene = (UIWindowScene*)scene;
3699 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3700 UIWindow* window = windowScene.windows[0];
3701 [window addSubview:viewController.view];
3703 [viewController loadView];
3706 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3708 [inputView setTextInputClient:123];
3709 [inputView reloadInputViews];
3710 [inputView becomeFirstResponder];
3712 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3713 [NSNotificationCenter.defaultCenter
3714 postNotificationName:UIKeyboardWillShowNotification
3716 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3720 [textInputPlugin handleMethodCall:onPointerMoveCall
3721 result:^(id _Nullable result){
3725 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3730 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3731 result:^(id _Nullable result){
3735 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3737 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3738 [subView removeFromSuperview];
3743 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3744 if (@available(iOS 17.0, *)) {
3745 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3746 "https://github.com/flutter/flutter/issues/183473");
3748 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3749 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3750 UIScene* scene = scenes.anyObject;
3751 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3752 UIWindowScene* windowScene = (UIWindowScene*)scene;
3753 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3754 UIWindow* window = windowScene.windows[0];
3755 [window addSubview:viewController.view];
3757 [viewController loadView];
3760 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3762 [inputView setTextInputClient:123];
3763 [inputView reloadInputViews];
3764 [inputView becomeFirstResponder];
3766 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3767 [NSNotificationCenter.defaultCenter
3768 postNotificationName:UIKeyboardWillShowNotification
3770 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3774 [textInputPlugin handleMethodCall:onPointerMoveCall
3775 result:^(id _Nullable result){
3778 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3783 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3784 result:^(id _Nullable result){
3787 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3792 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3793 result:^(id _Nullable result){
3796 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3797 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3798 [subView removeFromSuperview];
3803 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3805 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3806 [inputView setTextInputClient:123];
3807 [inputView reloadInputViews];
3808 [inputView becomeFirstResponder];
3810 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3811 XCTAssertEqualObjects(inputView, firstResponder);
3815 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3822 [subInputView addSubview:subFirstResponderInputView];
3823 [inputView addSubview:subInputView];
3824 [inputView addSubview:otherSubInputView];
3825 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3826 [inputView setTextInputClient:123];
3827 [inputView reloadInputViews];
3828 [subInputView setTextInputClient:123];
3829 [subInputView reloadInputViews];
3830 [otherSubInputView setTextInputClient:123];
3831 [otherSubInputView reloadInputViews];
3832 [subFirstResponderInputView setTextInputClient:123];
3833 [subFirstResponderInputView reloadInputViews];
3834 [subFirstResponderInputView becomeFirstResponder];
3836 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3837 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3841 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3843 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3844 [inputView setTextInputClient:123];
3845 [inputView reloadInputViews];
3847 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3848 XCTAssertNil(firstResponder);
3852 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3853 if (@available(iOS 17.0, *)) {
3854 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3855 "https://github.com/flutter/flutter/issues/183473");
3857 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3858 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3859 UIScene* scene = scenes.anyObject;
3860 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3861 UIWindowScene* windowScene = (UIWindowScene*)scene;
3862 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3863 UIWindow* window = windowScene.windows[0];
3864 [window addSubview:viewController.view];
3866 [viewController loadView];
3868 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3869 initWithDescription:
3870 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3871 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3872 .andDo(^(NSInvocation* invocation) {
3873 [expectation fulfill];
3875 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3876 [NSNotificationCenter.defaultCenter
3877 postNotificationName:UIKeyboardWillShowNotification
3879 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3883 [textInputPlugin handleMethodCall:initialMoveCall
3884 result:^(id _Nullable result){
3889 [textInputPlugin handleMethodCall:subsequentMoveCall
3890 result:^(id _Nullable result){
3896 [textInputPlugin handleMethodCall:pointerUpCall
3897 result:^(id _Nullable result){
3900 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3904 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3905 if (@available(iOS 17.0, *)) {
3906 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3907 "https://github.com/flutter/flutter/issues/183473");
3909 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3910 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3911 UIScene* scene = scenes.anyObject;
3912 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3913 UIWindowScene* windowScene = (UIWindowScene*)scene;
3914 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3915 UIWindow* window = windowScene.windows[0];
3916 [window addSubview:viewController.view];
3918 [viewController loadView];
3920 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3921 [NSNotificationCenter.defaultCenter
3922 postNotificationName:UIKeyboardWillShowNotification
3924 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3928 [textInputPlugin handleMethodCall:initialMoveCall
3929 result:^(id _Nullable result){
3934 [textInputPlugin handleMethodCall:subsequentMoveCall
3935 result:^(id _Nullable result){
3941 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3942 result:^(id _Nullable result){
3948 [textInputPlugin handleMethodCall:pointerUpCall
3949 result:^(id _Nullable result){
3951 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3952 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3954 XCTNSPredicateExpectation* expectation =
3955 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3956 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3960 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3961 if (@available(iOS 17.0, *)) {
3962 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3963 "https://github.com/flutter/flutter/issues/183473");
3965 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3966 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3967 UIScene* scene = scenes.anyObject;
3968 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3969 UIWindowScene* windowScene = (UIWindowScene*)scene;
3970 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3971 UIWindow* window = windowScene.windows[0];
3972 [window addSubview:viewController.view];
3974 [viewController loadView];
3977 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3979 [inputView setTextInputClient:123];
3980 [inputView reloadInputViews];
3981 [inputView becomeFirstResponder];
3983 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3984 [NSNotificationCenter.defaultCenter
3985 postNotificationName:UIKeyboardWillShowNotification
3987 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3991 [textInputPlugin handleMethodCall:initialMoveCall
3992 result:^(id _Nullable result){
3997 [textInputPlugin handleMethodCall:subsequentMoveCall
3998 result:^(id _Nullable result){
4004 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
4005 result:^(id _Nullable result){
4011 [textInputPlugin handleMethodCall:pointerUpCall
4012 result:^(id _Nullable result){
4014 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4015 return textInputPlugin.cachedFirstResponder.isFirstResponder;
4017 XCTNSPredicateExpectation* expectation =
4018 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4019 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4023 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
4024 if (@available(iOS 17.0, *)) {
4025 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4026 "https://github.com/flutter/flutter/issues/183473");
4028 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4029 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4030 UIScene* scene = scenes.anyObject;
4031 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
4032 UIWindowScene* windowScene = (UIWindowScene*)scene;
4033 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4034 UIWindow* window = windowScene.windows[0];
4035 [window addSubview:viewController.view];
4037 [viewController loadView];
4039 XCTestExpectation* expectation =
4040 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4041 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4042 [NSNotificationCenter.defaultCenter
4043 postNotificationName:UIKeyboardWillShowNotification
4045 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4049 [textInputPlugin handleMethodCall:initialMoveCall
4050 result:^(id _Nullable result){
4055 [textInputPlugin handleMethodCall:subsequentMoveCall
4056 result:^(id _Nullable result){
4061 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
4062 result:^(id _Nullable result){
4069 handleMethodCall:pointerUpCall
4070 result:^(id _Nullable result) {
4071 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4072 viewController.flutterScreenIfViewLoaded.bounds.size.height -
4073 keyboardFrame.origin.y);
4074 [expectation fulfill];
4079 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
4080 if (@available(iOS 17.0, *)) {
4081 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4082 "https://github.com/flutter/flutter/issues/183473");
4084 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4085 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4086 UIScene* scene = scenes.anyObject;
4087 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
4088 UIWindowScene* windowScene = (UIWindowScene*)scene;
4089 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4090 UIWindow* window = windowScene.windows[0];
4091 [window addSubview:viewController.view];
4093 [viewController loadView];
4095 XCTestExpectation* expectation =
4096 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4097 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4098 [NSNotificationCenter.defaultCenter
4099 postNotificationName:UIKeyboardWillShowNotification
4101 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4105 [textInputPlugin handleMethodCall:initialMoveCall
4106 result:^(id _Nullable result){
4111 [textInputPlugin handleMethodCall:subsequentMoveCall
4112 result:^(id _Nullable result){
4119 handleMethodCall:pointerUpCall
4120 result:^(id _Nullable result) {
4121 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4122 viewController.flutterScreenIfViewLoaded.bounds.size.height);
4123 [expectation fulfill];
4127 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
4128 [UIView setAnimationsEnabled:YES];
4129 [textInputPlugin showKeyboardAndRemoveScreenshot];
4131 UIView.areAnimationsEnabled,
4132 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
4135 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
4136 [UIView setAnimationsEnabled:YES];
4137 [textInputPlugin showKeyboardAndRemoveScreenshot];
4139 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4141 return UIView.areAnimationsEnabled;
4143 XCTNSPredicateExpectation* expectation =
4144 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4145 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4148 - (void)testEditMenu_shouldCreateCustomMenuItemWithCorrectProperties {
4149 if (@available(iOS 16.0, *)) {
4154 [myViewController loadView];
4158 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4160 result:^(id _Nullable result){
4165 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4167 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4168 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4170 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4171 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4173 NSArray<NSDictionary*>* encodedItems = @[
4174 @{@"type" : @"custom", @"id" : @"custom-action-1", @"title" : @"Custom Action 1"},
4175 @{@"type" : @"custom", @"id" : @"custom-action-2", @"title" : @"Custom Action 2"},
4178 BOOL shownEditMenu =
4179 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4180 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4182 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4183 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4184 suggestedActions:@[]];
4186 XCTAssertEqual(menu.children.count, 2UL,
@"Should create 2 custom menu items");
4187 UIAction* firstAction = (UIAction*)menu.children[0];
4188 UIAction* secondAction = (UIAction*)menu.children[1];
4189 XCTAssertEqualObjects(firstAction.title,
@"Custom Action 1",
4190 @"First action title should match");
4191 XCTAssertEqualObjects(secondAction.title,
@"Custom Action 2",
4192 @"Second action title should match");
4196 - (void)testEditMenu_customActionShouldTriggerDelegateCallback {
4197 if (@available(iOS 16.0, *)) {
4200 OCMStub([mockEngine platformChannel]).andReturn(mockPlatformChannel);
4202 OCMStub([mockEngine flutterTextInputView:[OCMArg any]
4203 performContextMenuCustomActionWithActionID:
@"test-callback-id"
4204 textInputClient:123])
4205 .andDo((^(NSInvocation* invocation) {
4206 [mockPlatformChannel invokeMethod:@"ContextMenu.onPerformCustomAction"
4207 arguments:@[ @(123), @"test-callback-id" ]];
4214 [myViewController loadView];
4218 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4220 result:^(id _Nullable result){
4225 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4226 XCTestExpectation* expectation = [[XCTestExpectation alloc]
4227 initWithDescription:@"Custom action delegate callback should be called"];
4228 OCMStub(([mockPlatformChannel invokeMethod:
@"ContextMenu.onPerformCustomAction"
4229 arguments:@[ @(123),
@"test-callback-id" ]]))
4230 .andDo(^(NSInvocation* invocation) {
4231 [expectation fulfill];
4233 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4234 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4236 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4237 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4239 NSArray<NSDictionary*>* encodedItems = @[
4240 @{@"type" : @"custom", @"id" : @"test-callback-id", @"title" : @"Test Action"},
4243 BOOL shownEditMenu =
4244 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4245 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4247 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4248 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4249 suggestedActions:@[]];
4251 XCTAssertEqual(menu.children.count, 1UL,
@"Should have 1 custom menu item");
4252 UIAction* customAction = (UIAction*)menu.children[0];
4253 XCTAssertEqualObjects(customAction.title,
@"Test Action",
@"Action title should match");
4255 [myInputView.textInputDelegate flutterTextInputView:myInputView
4256 performContextMenuCustomActionWithActionID:@"test-callback-id"
4257 textInputClient:123];
4259 [
self waitForExpectations:@[ expectation ] timeout:1.0];
4260 OCMVerifyAll(mockPlatformChannel);
NSArray< FlutterTextSelectionRect * > * selectionRects
UITextRange * markedTextRange
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
CGRect caretRectForPosition
const CGRect kInvalidFirstRect
NSDictionary * _passwordTemplate
FlutterViewController * viewController
FlutterTextInputPlugin * textInputPlugin
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
FlutterViewController * viewController
void flutterTextInputView:performAction:withClient:(FlutterTextInputView *textInputView,[performAction] FlutterTextInputAction action,[withClient] int client)
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
UIView< UITextInput > * textInputView()
UIIndirectScribbleInteractionDelegate UIViewController * viewController
BOOL showEditMenu:(ios(16.0) API_AVAILABLE)
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
UIAccessibilityNotifications receivedNotification
id receivedNotificationTarget
BOOL isAccessibilityFocused
instancetype positionWithIndex:(NSUInteger index)
UITextStorageDirection affinity
instancetype rangeWithNSRange:(NSRange range)
instancetype selectionRectWithRect:position:(CGRect rect,[position] NSUInteger position)
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)