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)
73 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
75 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
76 clearText:(BOOL)clearText
77 delayRemoval:(BOOL)delayRemoval;
78 - (NSArray<UIView*>*)textInputViews;
81 - (void)startLiveTextInput;
82 - (void)showKeyboardAndRemoveScreenshot;
88 class MockPlatformViewDelegate :
public PlatformView::Delegate {
90 void OnPlatformViewCreated(std::unique_ptr<Surface> surface)
override {}
91 void OnPlatformViewDestroyed()
override {}
92 void OnPlatformViewScheduleFrame()
override {}
93 void OnPlatformViewAddView(int64_t view_id,
94 const ViewportMetrics& viewport_metrics,
95 AddViewCallback callback)
override {}
96 void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback)
override {}
97 void OnPlatformViewSendViewFocusEvent(
const ViewFocusEvent& event)
override {};
98 void OnPlatformViewSetNextFrameCallback(
const fml::closure& closure)
override {}
99 void OnPlatformViewSetViewportMetrics(int64_t view_id,
const ViewportMetrics& metrics)
override {}
100 const flutter::Settings& OnPlatformViewGetSettings()
const override {
return settings_; }
101 void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage> message)
override {}
102 void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet)
override {
104 void OnPlatformViewDispatchSemanticsAction(int64_t view_id,
106 SemanticsAction action,
107 fml::MallocMapping args)
override {}
108 void OnPlatformViewSetSemanticsEnabled(
bool enabled)
override {}
109 void OnPlatformViewSetAccessibilityFeatures(int32_t flags)
override {}
110 void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture)
override {}
111 void OnPlatformViewUnregisterTexture(int64_t
texture_id)
override {}
112 void OnPlatformViewMarkTextureFrameAvailable(int64_t
texture_id)
override {}
114 void LoadDartDeferredLibrary(intptr_t loading_unit_id,
115 std::unique_ptr<const fml::Mapping> snapshot_data,
116 std::unique_ptr<const fml::Mapping> snapshot_instructions)
override {
118 void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
119 const std::string error_message,
120 bool transient)
override {}
121 void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
122 flutter::AssetResolver::AssetResolverType type)
override {}
134 NSDictionary* _template;
152 UIPasteboard.generalPasteboard.items = @[];
158 [textInputPlugin.autofillContext removeAllObjects];
159 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
160 [[[[textInputPlugin textInputView] superview] subviews]
161 makeObjectsPerformSelector:@selector(removeFromSuperview)];
166 - (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
169 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
170 [textInputPlugin handleMethodCall:setClientCall
171 result:^(id _Nullable result){
175 - (void)setTextInputShow {
178 [textInputPlugin handleMethodCall:setClientCall
179 result:^(id _Nullable result){
183 - (void)setTextInputHide {
186 [textInputPlugin handleMethodCall:setClientCall
187 result:^(id _Nullable result){
191 - (void)flushScheduledAsyncBlocks {
192 __block
bool done =
false;
193 XCTestExpectation* expectation =
194 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
195 dispatch_async(dispatch_get_main_queue(), ^{
198 dispatch_async(dispatch_get_main_queue(), ^{
200 [expectation fulfill];
202 [
self waitForExpectations:@[ expectation ] timeout:10];
205 - (NSMutableDictionary*)mutableTemplateCopy {
208 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
209 @"keyboardAppearance" :
@"Brightness.light",
210 @"obscureText" : @NO,
211 @"inputAction" :
@"TextInputAction.unspecified",
212 @"smartDashesType" :
@"0",
213 @"smartQuotesType" :
@"0",
214 @"autocorrect" : @YES,
215 @"enableInteractiveSelection" : @YES,
219 return [_template mutableCopy];
223 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
224 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
228 - (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
229 atIndex:(NSInteger)index {
232 withGranularity:UITextGranularityLine
233 inDirection:UITextLayoutDirectionRight];
238 - (void)updateConfig:(NSDictionary*)config {
241 [textInputPlugin handleMethodCall:updateConfigCall
242 result:^(id _Nullable result){
248 - (void)testWillNotCrashWhenViewControllerIsNil {
255 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
258 result:^(id _Nullable result) {
259 XCTAssertNil(result);
260 [expectation fulfill];
262 XCTAssertNil(inputPlugin.activeView);
263 [
self waitForExpectations:@[ expectation ] timeout:1.0];
266 - (void)testInvokeStartLiveTextInput {
271 result:^(id _Nullable result){
273 OCMVerify([mockPlugin startLiveTextInput]);
276 - (void)testNoDanglingEnginePointer {
286 weakFlutterEngine = flutterEngine;
287 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
289 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
290 weakFlutterTextInputPlugin = flutterTextInputPlugin;
294 NSDictionary* config =
self.mutableTemplateCopy;
297 arguments:@[ [NSNumber numberWithInt:123], config ]];
299 result:^(id _Nullable result){
301 currentView = flutterTextInputPlugin.activeView;
304 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
305 XCTAssertNotNil(currentView,
@"current view must not be nil");
307 XCTAssertNil(weakFlutterTextInputPlugin);
310 XCTAssertNil(currentView.textInputDelegate);
313 - (void)testSecureInput {
314 NSDictionary* config =
self.mutableTemplateCopy;
315 [config setValue:@"YES" forKey:@"obscureText"];
316 [
self setClientId:123 configuration:config];
319 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
326 XCTAssertTrue(inputView.secureTextEntry);
329 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
332 XCTAssertEqual(inputFields.count, 1ul);
340 XCTAssert(inputView.autofillId.length > 0);
343 - (void)testKeyboardType {
344 NSDictionary* config =
self.mutableTemplateCopy;
345 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
346 [
self setClientId:123 configuration:config];
349 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
354 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
357 - (void)testKeyboardTypeWebSearch {
358 NSDictionary* config =
self.mutableTemplateCopy;
359 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
360 [
self setClientId:123 configuration:config];
363 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
368 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
371 - (void)testKeyboardTypeTwitter {
372 NSDictionary* config =
self.mutableTemplateCopy;
373 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
374 [
self setClientId:123 configuration:config];
377 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
382 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
385 - (void)testVisiblePasswordUseAlphanumeric {
386 NSDictionary* config =
self.mutableTemplateCopy;
387 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
388 [
self setClientId:123 configuration:config];
391 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
396 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
399 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
400 NSDictionary* config =
self.mutableTemplateCopy;
401 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
402 [
self setClientId:123 configuration:config];
407 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
408 [
self setClientId:124 configuration:config];
413 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
417 if (@available(iOS 17.0, *)) {
419 OCMVerify(never(), [
engine flutterTextInputView:inputView
420 showAutocorrectionPromptRectForStart:0
424 OCMVerify([
engine flutterTextInputView:inputView
425 showAutocorrectionPromptRectForStart:0
431 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
433 __block
int updateCount = 0;
434 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
435 .andDo(^(NSInvocation* invocation) {
439 [inputView.text setString:@"Some initial text"];
440 XCTAssertEqual(updateCount, 0);
443 [inputView setSelectedTextRange:textRange];
444 XCTAssertEqual(updateCount, 1);
447 NSDictionary* config =
self.mutableTemplateCopy;
448 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
449 [config setValue:@(NO) forKey:@"obscureText"];
450 [config setValue:@(NO) forKey:@"enableDeltaModel"];
451 [inputView configureWithDictionary:config];
454 [inputView setSelectedTextRange:textRange];
456 XCTAssertEqual(updateCount, 1);
459 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
461 if (@available(iOS 17.0, *)) {
465 if (@available(iOS 14.0, *)) {
468 __block
int callCount = 0;
469 OCMStub([
engine flutterTextInputView:inputView
470 showAutocorrectionPromptRectForStart:0
473 .andDo(^(NSInvocation* invocation) {
479 XCTAssertEqual(callCount, 1);
481 UIScribbleInteraction* scribbleInteraction =
482 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
484 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
488 XCTAssertEqual(callCount, 1);
490 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
491 [inputView resetScribbleInteractionStatusIfEnding];
494 XCTAssertEqual(callCount, 2);
496 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
500 XCTAssertEqual(callCount, 2);
502 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
506 XCTAssertEqual(callCount, 2);
508 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
511 XCTAssertEqual(callCount, 3);
515 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
521 arguments:@[ @(123),
self.mutableTemplateCopy ]];
523 result:^(id _Nullable result){
530 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
534 arguments:@{@"transform" : yOffsetMatrix}];
536 result:^(id _Nullable result){
539 if (@available(iOS 17, *)) {
540 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
541 @"The input hider should overlap with the text on and after iOS 17");
544 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
545 @"The input hider should be on the origin of screen on and before iOS 16.");
549 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
555 toPosition:toPosition];
556 NSRange range = flutterRange.
range;
558 XCTAssertEqual(range.location, 0ul);
559 XCTAssertEqual(range.length, 2ul);
562 - (void)testTextInRange {
563 NSDictionary* config =
self.mutableTemplateCopy;
564 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
565 [
self setClientId:123 configuration:config];
566 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
569 [inputView insertText:@"test"];
572 NSString* substring = [inputView textInRange:range];
573 XCTAssertEqual(substring.length, 4ul);
576 substring = [inputView textInRange:range];
577 XCTAssertEqual(substring.length, 0ul);
580 - (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
581 NSDictionary* config =
self.mutableTemplateCopy;
582 [
self setClientId:123 configuration:config];
583 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
586 [inputView insertText:@"text"];
589 NSString* substring = [inputView textInRange:range];
590 XCTAssertNil(substring);
593 - (void)testStandardEditActions {
594 NSDictionary* config =
self.mutableTemplateCopy;
595 [
self setClientId:123 configuration:config];
596 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
599 [inputView insertText:@"aaaa"];
600 [inputView selectAll:nil];
602 [inputView insertText:@"bbbb"];
603 XCTAssertTrue([inputView canPerformAction:
@selector(paste:) withSender:nil]);
604 [inputView paste:nil];
605 [inputView selectAll:nil];
606 [inputView copy:nil];
607 [inputView paste:nil];
608 [inputView selectAll:nil];
609 [inputView delete:nil];
610 [inputView paste:nil];
611 [inputView paste:nil];
614 NSString* substring = [inputView textInRange:range];
615 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
618 - (void)testCanPerformActionForSelectActions {
619 NSDictionary* config =
self.mutableTemplateCopy;
620 [
self setClientId:123 configuration:config];
621 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
624 XCTAssertFalse([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
626 [inputView insertText:@"aaaa"];
628 XCTAssertTrue([inputView canPerformAction:
@selector(selectAll:) withSender:nil]);
631 - (void)testCanPerformActionCaptureTextFromCamera {
632 if (@available(iOS 15.0, *)) {
633 NSDictionary* config =
self.mutableTemplateCopy;
634 [
self setClientId:123 configuration:config];
635 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
638 [inputView becomeFirstResponder];
639 XCTAssertTrue([inputView canPerformAction:
@selector(captureTextFromCamera:) withSender:nil]);
641 [inputView insertText:@"test"];
642 [inputView selectAll:nil];
643 XCTAssertTrue([inputView canPerformAction:
@selector(captureTextFromCamera:) withSender:nil]);
647 - (void)testDeletingBackward {
648 NSDictionary* config =
self.mutableTemplateCopy;
649 [
self setClientId:123 configuration:config];
650 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
653 [inputView insertText:@"á ž¹ð Ÿ˜€ text 𠟥°ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦ð Ÿ‡ºð Ÿ‡³à ¸”à ¸µ "];
654 [inputView deleteBackward];
655 [inputView deleteBackward];
658 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³à¸”");
659 [inputView deleteBackward];
660 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³");
661 [inputView deleteBackward];
662 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦");
663 [inputView deleteBackward];
664 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
665 [inputView deleteBackward];
667 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
668 [inputView deleteBackward];
669 [inputView deleteBackward];
670 [inputView deleteBackward];
671 [inputView deleteBackward];
672 [inputView deleteBackward];
673 [inputView deleteBackward];
675 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
676 [inputView deleteBackward];
677 XCTAssertEqualObjects(inputView.text,
@"áž¹");
678 [inputView deleteBackward];
679 XCTAssertEqualObjects(inputView.text,
@"");
684 - (void)testSystemOnlyAddingPartialComposedCharacter {
685 NSDictionary* config =
self.mutableTemplateCopy;
686 [
self setClientId:123 configuration:config];
687 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
690 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
691 [inputView deleteBackward];
694 [inputView insertText:[@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)]];
695 [inputView insertText:@"ì •„"];
697 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ì•„");
700 [inputView deleteBackward];
703 [inputView insertText:@"𠟘€"];
704 [inputView deleteBackward];
706 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
707 [inputView insertText:@"ì •„"];
708 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
711 [inputView deleteBackward];
714 [inputView deleteBackward];
716 [inputView insertText:[@"𠟘€" substringWithRange:NSMakeRange(0, 1)]];
717 [inputView insertText:@"ì •„"];
719 XCTAssertEqualObjects(inputView.text,
@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
722 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
723 NSDictionary* config =
self.mutableTemplateCopy;
724 [
self setClientId:123 configuration:config];
725 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
728 [inputView insertText:@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦"];
729 [inputView deleteBackward];
730 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
733 NSString* brokenEmoji = [@"ð Ÿ‘¨â €ð Ÿ‘©â €ð Ÿ‘§â €ð Ÿ‘¦" substringWithRange:NSMakeRange(0, 1)];
734 [inputView insertText:brokenEmoji];
735 [inputView insertText:@"ì •„"];
737 NSString* finalText = [NSString stringWithFormat:@"%@ì •„", brokenEmoji];
738 XCTAssertEqualObjects(inputView.text, finalText);
741 - (void)testPastingNonTextDisallowed {
742 NSDictionary* config =
self.mutableTemplateCopy;
743 [
self setClientId:123 configuration:config];
744 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
747 UIPasteboard.generalPasteboard.color = UIColor.redColor;
748 XCTAssertNil(UIPasteboard.generalPasteboard.string);
749 XCTAssertFalse([inputView canPerformAction:
@selector(paste:) withSender:nil]);
750 [inputView paste:nil];
752 XCTAssertEqualObjects(inputView.text,
@"");
755 - (void)testNoZombies {
762 [passwordView.textField description];
764 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
767 - (void)testInputViewCrash {
772 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
773 activeView = inputPlugin.activeView;
775 [activeView updateEditingState];
778 - (void)testDoNotReuseInputViews {
779 NSDictionary* config =
self.mutableTemplateCopy;
780 [
self setClientId:123 configuration:config];
782 [
self setClientId:456 configuration:config];
784 XCTAssertNotNil(currentView);
789 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
791 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
795 - (void)testPropagatePressEventsToViewController {
797 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
798 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
802 NSDictionary* config =
self.mutableTemplateCopy;
803 [
self setClientId:123 configuration:config];
805 [
self setTextInputShow];
807 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
808 withEvent:OCMClassMock([UIPressesEvent class])];
810 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
811 withEvent:[OCMArg isNotNil]]);
812 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
813 withEvent:[OCMArg isNotNil]]);
815 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
816 withEvent:OCMClassMock([UIPressesEvent class])];
818 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
819 withEvent:[OCMArg isNotNil]]);
820 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
821 withEvent:[OCMArg isNotNil]]);
824 - (void)testPropagatePressEventsToViewController2 {
826 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
827 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
831 NSDictionary* config =
self.mutableTemplateCopy;
832 [
self setClientId:123 configuration:config];
833 [
self setTextInputShow];
836 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
837 withEvent:OCMClassMock([UIPressesEvent class])];
839 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
840 withEvent:[OCMArg isNotNil]]);
841 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
842 withEvent:[OCMArg isNotNil]]);
845 [
self setClientId:321 configuration:config];
846 [
self setTextInputShow];
848 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
850 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
851 withEvent:OCMClassMock([UIPressesEvent class])];
853 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
854 withEvent:[OCMArg isNotNil]]);
855 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
856 withEvent:[OCMArg isNotNil]]);
859 - (void)testHotRestart {
860 flutter::MockPlatformViewDelegate mock_platform_view_delegate;
861 auto thread = std::make_unique<fml::Thread>(
"TextInputHotRestart");
862 auto thread_task_runner = thread->GetTaskRunner();
863 flutter::TaskRunners runners(
self.name.UTF8String,
868 id mockFlutterView = OCMClassMock([
FlutterView class]);
871 OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
872 OCMStub([mockFlutterViewController
textInputPlugin]).andReturn(mockFlutterTextInputPlugin);
874 fml::AutoResetWaitableEvent latch;
875 thread_task_runner->PostTask([&] {
876 auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
877 mock_platform_view_delegate,
878 mock_platform_view_delegate.settings_.enable_impeller
884 std::make_shared<fml::SyncSwitch>());
886 platform_view->SetOwnerViewController(mockFlutterViewController);
888 OCMExpect([mockFlutterTextInputPlugin reset]);
890 OCMVerifyAll(mockFlutterView);
897 - (void)testUpdateSecureTextEntry {
898 NSDictionary* config =
self.mutableTemplateCopy;
899 [config setValue:@"YES" forKey:@"obscureText"];
900 [
self setClientId:123 configuration:config];
902 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
905 __block
int callCount = 0;
906 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
910 XCTAssertTrue(inputView.isSecureTextEntry);
912 config =
self.mutableTemplateCopy;
913 [config setValue:@"NO" forKey:@"obscureText"];
914 [
self updateConfig:config];
916 XCTAssertEqual(callCount, 1);
917 XCTAssertFalse(inputView.isSecureTextEntry);
920 - (void)testInputActionContinueAction {
936 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
938 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
941 - (void)testDisablingAutocorrectDisablesSpellChecking {
945 NSDictionary* config =
self.mutableTemplateCopy;
946 [inputView configureWithDictionary:config];
948 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
949 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
951 [config setValue:@(NO) forKey:@"autocorrect"];
952 [inputView configureWithDictionary:config];
954 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
955 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
958 - (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
960 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
974 XCTAssertEqual(inputView.markedTextRange, nil);
977 - (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
981 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
982 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
983 if (@available(iOS 17, *)) {
984 XCTAssertFalse(respondsToInsertionPointColor);
986 XCTAssertTrue(respondsToInsertionPointColor);
990 #pragma mark - TextEditingDelta tests
991 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
993 inputView.enableDeltaModel = YES;
995 __block
int updateCount = 0;
997 [inputView insertText:@"text to insert"];
1000 flutterTextInputView:inputView
1001 updateEditingClient:0
1002 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1003 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1004 isEqualToString:
@""]) &&
1005 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1006 isEqualToString:
@"text to insert"]) &&
1007 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1008 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
1010 .andDo(^(NSInvocation* invocation) {
1013 XCTAssertEqual(updateCount, 0);
1015 [
self flushScheduledAsyncBlocks];
1018 XCTAssertEqual(updateCount, 1);
1020 [inputView deleteBackward];
1021 OCMExpect([
engine flutterTextInputView:inputView
1022 updateEditingClient:0
1023 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1024 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1025 isEqualToString:
@"text to insert"]) &&
1026 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1027 isEqualToString:
@""]) &&
1028 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1030 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1033 .andDo(^(NSInvocation* invocation) {
1036 [
self flushScheduledAsyncBlocks];
1037 XCTAssertEqual(updateCount, 2);
1040 OCMExpect([
engine flutterTextInputView:inputView
1041 updateEditingClient:0
1042 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1043 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1044 isEqualToString:
@"text to inser"]) &&
1045 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1046 isEqualToString:
@""]) &&
1047 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1049 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1052 .andDo(^(NSInvocation* invocation) {
1055 [
self flushScheduledAsyncBlocks];
1056 XCTAssertEqual(updateCount, 3);
1059 withText:@"replace text"];
1062 flutterTextInputView:inputView
1063 updateEditingClient:0
1064 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1065 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1066 isEqualToString:
@"text to inser"]) &&
1067 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1068 isEqualToString:
@"replace text"]) &&
1069 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1070 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
1072 .andDo(^(NSInvocation* invocation) {
1075 [
self flushScheduledAsyncBlocks];
1076 XCTAssertEqual(updateCount, 4);
1078 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1079 OCMExpect([
engine flutterTextInputView:inputView
1080 updateEditingClient:0
1081 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1082 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1083 isEqualToString:
@"replace textext to inser"]) &&
1084 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1085 isEqualToString:
@"marked text"]) &&
1086 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1088 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1091 .andDo(^(NSInvocation* invocation) {
1094 [
self flushScheduledAsyncBlocks];
1095 XCTAssertEqual(updateCount, 5);
1097 [inputView unmarkText];
1099 flutterTextInputView:inputView
1100 updateEditingClient:0
1101 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1102 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1103 isEqualToString:
@"replace textmarked textext to inser"]) &&
1104 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1105 isEqualToString:
@""]) &&
1106 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1108 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1111 .andDo(^(NSInvocation* invocation) {
1114 [
self flushScheduledAsyncBlocks];
1116 XCTAssertEqual(updateCount, 6);
1120 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1123 inputView.enableDeltaModel = YES;
1126 OCMExpect([
engine flutterTextInputView:inputView
1127 updateEditingClient:0
1128 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1129 NSArray* deltas = state[@"deltas"];
1130 NSDictionary* firstDelta = deltas[0];
1131 NSDictionary* secondDelta = deltas[1];
1132 NSDictionary* thirdDelta = deltas[2];
1133 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1134 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1135 [firstDelta[@"deltaStart"] intValue] == 0 &&
1136 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1137 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1138 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1139 [secondDelta[@"deltaStart"] intValue] == 0 &&
1140 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1141 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1142 [thirdDelta[@"deltaText"] isEqualToString:@"â €”"] &&
1143 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1144 [thirdDelta[@"deltaEnd"] intValue] == 0;
1148 [inputView insertText:@"-"];
1149 [inputView deleteBackward];
1150 [inputView insertText:@"â €”"];
1152 [
self flushScheduledAsyncBlocks];
1156 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1158 inputView.enableDeltaModel = YES;
1160 __block
int updateCount = 0;
1161 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1162 .andDo(^(NSInvocation* invocation) {
1166 [inputView.text setString:@"Some initial text"];
1167 XCTAssertEqual(updateCount, 0);
1170 inputView.markedTextRange = range;
1171 inputView.selectedTextRange = nil;
1172 [
self flushScheduledAsyncBlocks];
1173 XCTAssertEqual(updateCount, 1);
1175 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1177 flutterTextInputView:inputView
1178 updateEditingClient:0
1179 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1180 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1181 isEqualToString:
@"Some initial text"]) &&
1182 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1183 isEqualToString:
@"new marked text."]) &&
1184 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1185 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1187 [
self flushScheduledAsyncBlocks];
1188 XCTAssertEqual(updateCount, 2);
1191 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1193 inputView.enableDeltaModel = YES;
1195 __block
int updateCount = 0;
1196 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1197 .andDo(^(NSInvocation* invocation) {
1201 [inputView.text setString:@"Some initial text"];
1202 [
self flushScheduledAsyncBlocks];
1203 XCTAssertEqual(updateCount, 0);
1206 inputView.markedTextRange = range;
1207 inputView.selectedTextRange = nil;
1208 [
self flushScheduledAsyncBlocks];
1209 XCTAssertEqual(updateCount, 1);
1211 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1213 flutterTextInputView:inputView
1214 updateEditingClient:0
1215 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1216 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1217 isEqualToString:
@"Some initial text"]) &&
1218 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1219 isEqualToString:
@"text."]) &&
1220 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1221 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1223 [
self flushScheduledAsyncBlocks];
1224 XCTAssertEqual(updateCount, 2);
1227 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1229 inputView.enableDeltaModel = YES;
1231 __block
int updateCount = 0;
1232 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1233 .andDo(^(NSInvocation* invocation) {
1237 [inputView.text setString:@"Some initial text"];
1238 [
self flushScheduledAsyncBlocks];
1239 XCTAssertEqual(updateCount, 0);
1242 inputView.markedTextRange = range;
1243 inputView.selectedTextRange = nil;
1244 [
self flushScheduledAsyncBlocks];
1245 XCTAssertEqual(updateCount, 1);
1247 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1249 flutterTextInputView:inputView
1250 updateEditingClient:0
1251 withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1252 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1253 isEqualToString:
@"Some initial text"]) &&
1254 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1255 isEqualToString:
@"tex"]) &&
1256 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1257 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1259 [
self flushScheduledAsyncBlocks];
1260 XCTAssertEqual(updateCount, 2);
1263 #pragma mark - EditingState tests
1265 - (void)testUITextInputCallsUpdateEditingStateOnce {
1268 __block
int updateCount = 0;
1269 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1270 .andDo(^(NSInvocation* invocation) {
1274 [inputView insertText:@"text to insert"];
1276 XCTAssertEqual(updateCount, 1);
1278 [inputView deleteBackward];
1279 XCTAssertEqual(updateCount, 2);
1282 XCTAssertEqual(updateCount, 3);
1285 withText:@"replace text"];
1286 XCTAssertEqual(updateCount, 4);
1288 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1289 XCTAssertEqual(updateCount, 5);
1291 [inputView unmarkText];
1292 XCTAssertEqual(updateCount, 6);
1295 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1297 inputView.enableDeltaModel = YES;
1299 __block
int updateCount = 0;
1300 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1301 .andDo(^(NSInvocation* invocation) {
1305 [inputView insertText:@"text to insert"];
1306 [
self flushScheduledAsyncBlocks];
1308 XCTAssertEqual(updateCount, 1);
1310 [inputView deleteBackward];
1311 [
self flushScheduledAsyncBlocks];
1312 XCTAssertEqual(updateCount, 2);
1315 [
self flushScheduledAsyncBlocks];
1316 XCTAssertEqual(updateCount, 3);
1319 withText:@"replace text"];
1320 [
self flushScheduledAsyncBlocks];
1321 XCTAssertEqual(updateCount, 4);
1323 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1324 [
self flushScheduledAsyncBlocks];
1325 XCTAssertEqual(updateCount, 5);
1327 [inputView unmarkText];
1328 [
self flushScheduledAsyncBlocks];
1329 XCTAssertEqual(updateCount, 6);
1332 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1335 __block
int updateCount = 0;
1336 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1337 .andDo(^(NSInvocation* invocation) {
1341 [inputView.text setString:@"BEFORE"];
1342 XCTAssertEqual(updateCount, 0);
1344 inputView.markedTextRange = nil;
1345 inputView.selectedTextRange = nil;
1346 XCTAssertEqual(updateCount, 1);
1349 XCTAssertEqual(updateCount, 1);
1350 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1351 XCTAssertEqual(updateCount, 1);
1352 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1353 XCTAssertEqual(updateCount, 1);
1357 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1358 XCTAssertEqual(updateCount, 1);
1360 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1361 XCTAssertEqual(updateCount, 1);
1365 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1366 XCTAssertEqual(updateCount, 1);
1368 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1369 XCTAssertEqual(updateCount, 1);
1372 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
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.text setString:@"BEFORE"];
1383 [
self flushScheduledAsyncBlocks];
1384 XCTAssertEqual(updateCount, 0);
1386 inputView.markedTextRange = nil;
1387 inputView.selectedTextRange = nil;
1388 [
self flushScheduledAsyncBlocks];
1389 XCTAssertEqual(updateCount, 1);
1392 XCTAssertEqual(updateCount, 1);
1393 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1394 [
self flushScheduledAsyncBlocks];
1395 XCTAssertEqual(updateCount, 1);
1397 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1398 [
self flushScheduledAsyncBlocks];
1399 XCTAssertEqual(updateCount, 1);
1403 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1404 [
self flushScheduledAsyncBlocks];
1405 XCTAssertEqual(updateCount, 1);
1408 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1409 [
self flushScheduledAsyncBlocks];
1410 XCTAssertEqual(updateCount, 1);
1414 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1415 [
self flushScheduledAsyncBlocks];
1416 XCTAssertEqual(updateCount, 1);
1419 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1420 [
self flushScheduledAsyncBlocks];
1421 XCTAssertEqual(updateCount, 1);
1424 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1427 __block
int updateCount = 0;
1428 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1429 .andDo(^(NSInvocation* invocation) {
1433 [inputView unmarkText];
1435 XCTAssertEqual(updateCount, 0);
1437 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1439 XCTAssertEqual(updateCount, 1);
1441 [inputView unmarkText];
1443 XCTAssertEqual(updateCount, 2);
1446 - (void)testCanCopyPasteWithScribbleEnabled {
1447 if (@available(iOS 14.0, *)) {
1448 NSDictionary* config =
self.mutableTemplateCopy;
1449 [
self setClientId:123 configuration:config];
1450 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1456 [mockInputView insertText:@"aaaa"];
1457 [mockInputView selectAll:nil];
1459 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1460 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1461 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1462 XCTAssertFalse([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1464 [mockInputView copy:NULL];
1465 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:NULL]);
1466 XCTAssertTrue([mockInputView canPerformAction:
@selector(copy:) withSender:
@"sender"]);
1467 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:NULL]);
1468 XCTAssertTrue([mockInputView canPerformAction:
@selector(paste:) withSender:
@"sender"]);
1472 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1473 if (@available(iOS 14.0, *)) {
1476 __block
int updateCount = 0;
1477 OCMStub([
engine flutterTextInputView:inputView
1478 updateEditingClient:0
1479 withState:[OCMArg isNotNil]])
1480 .andDo(^(NSInvocation* invocation) {
1484 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1486 XCTAssertEqual(updateCount, 1);
1488 UIScribbleInteraction* scribbleInteraction =
1489 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1491 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1492 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1494 XCTAssertEqual(updateCount, 1);
1496 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1497 [inputView resetScribbleInteractionStatusIfEnding];
1498 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1500 XCTAssertEqual(updateCount, 2);
1502 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1503 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1506 XCTAssertEqual(updateCount, 2);
1508 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1509 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1512 XCTAssertEqual(updateCount, 2);
1514 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1515 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1517 XCTAssertEqual(updateCount, 3);
1521 - (void)testUpdateEditingClientNegativeSelection {
1524 [inputView.text setString:@"SELECTION"];
1525 inputView.markedTextRange = nil;
1526 inputView.selectedTextRange = nil;
1528 [inputView setTextInputState:@{
1529 @"text" : @"SELECTION",
1530 @"selectionBase" : @-1,
1531 @"selectionExtent" : @-1
1533 [inputView updateEditingState];
1534 OCMVerify([
engine flutterTextInputView:inputView
1535 updateEditingClient:0
1536 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1537 return ([state[
@"selectionBase"] intValue]) == 0 &&
1538 ([state[
@"selectionExtent"] intValue] == 0);
1543 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1544 [inputView updateEditingState];
1545 OCMVerify([
engine flutterTextInputView:inputView
1546 updateEditingClient:0
1547 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1548 return ([state[
@"selectionBase"] intValue]) == 0 &&
1549 ([state[
@"selectionExtent"] intValue] == 0);
1553 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1554 [inputView updateEditingState];
1555 OCMVerify([
engine flutterTextInputView:inputView
1556 updateEditingClient:0
1557 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1558 return ([state[
@"selectionBase"] intValue]) == 0 &&
1559 ([state[
@"selectionExtent"] intValue] == 0);
1563 - (void)testUpdateEditingClientSelectionClamping {
1567 [inputView.text setString:@"SELECTION"];
1568 inputView.markedTextRange = nil;
1569 inputView.selectedTextRange = nil;
1572 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1573 [inputView updateEditingState];
1574 OCMVerify([
engine flutterTextInputView:inputView
1575 updateEditingClient:0
1576 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1577 return ([state[
@"selectionBase"] intValue]) == 0 &&
1578 ([state[
@"selectionExtent"] intValue] == 0);
1582 [inputView setTextInputState:@{
1583 @"text" : @"SELECTION",
1584 @"selectionBase" : @0,
1585 @"selectionExtent" : @9999
1587 [inputView updateEditingState];
1589 OCMVerify([
engine flutterTextInputView:inputView
1590 updateEditingClient:0
1591 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1592 return ([state[
@"selectionBase"] intValue]) == 0 &&
1593 ([state[
@"selectionExtent"] intValue] == 9);
1598 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1599 [inputView updateEditingState];
1600 OCMVerify([
engine flutterTextInputView:inputView
1601 updateEditingClient:0
1602 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1603 return ([state[
@"selectionBase"] intValue]) == 0 &&
1604 ([state[
@"selectionExtent"] intValue] == 1);
1608 [inputView setTextInputState:@{
1609 @"text" : @"SELECTION",
1610 @"selectionBase" : @9999,
1611 @"selectionExtent" : @9999
1613 [inputView updateEditingState];
1614 OCMVerify([
engine flutterTextInputView:inputView
1615 updateEditingClient:0
1616 withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1617 return ([state[
@"selectionBase"] intValue]) == 9 &&
1618 ([state[
@"selectionExtent"] intValue] == 9);
1622 - (void)testInputViewsHasNonNilInputDelegate {
1624 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1626 [inputView setTextInputClient:123];
1627 [inputView reloadInputViews];
1628 [inputView becomeFirstResponder];
1629 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1630 inputView.inputDelegate = nil;
1634 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1635 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1636 [inputView removeFromSuperview];
1639 - (void)testInputViewsDoNotHaveUITextInteractions {
1641 BOOL hasTextInteraction = NO;
1642 for (
id interaction in inputView.interactions) {
1643 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1644 if (hasTextInteraction) {
1648 XCTAssertFalse(hasTextInteraction);
1651 #pragma mark - UITextInput methods - Tests
1653 - (void)testUpdateFirstRectForRange {
1654 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1660 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1665 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1666 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1670 NSArray* affineMatrix = @[
1671 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1672 @(-6.0), @(3.0), @(9.0), @(1.0)
1676 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1678 [inputView setEditableTransform:yOffsetMatrix];
1680 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1683 CGRect testRect = CGRectMake(0, 0, 100, 100);
1684 [inputView setMarkedRect:testRect];
1686 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1687 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1689 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1692 [inputView setEditableTransform:zeroMatrix];
1694 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1695 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1698 [inputView setEditableTransform:yOffsetMatrix];
1699 [inputView setMarkedRect:testRect];
1700 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1703 [inputView setMarkedRect:kInvalidFirstRect];
1705 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1706 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1709 [inputView setEditableTransform:affineMatrix];
1710 [inputView setMarkedRect:testRect];
1712 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1714 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1715 const CGPoint offset = CGPointMake(113, 119);
1716 CGRect currentFrame = inputView.frame;
1717 currentFrame.origin = offset;
1718 inputView.frame = currentFrame;
1721 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1722 [inputView firstRectForRange:range]));
1725 - (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1727 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1732 [inputView setSelectionRects:@[
1741 if (@available(iOS 17, *)) {
1742 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1743 [inputView firstRectForRange:multiRectRange]));
1745 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1746 [inputView firstRectForRange:multiRectRange]));
1750 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1752 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1754 [inputView setSelectionRects:@[
1761 if (@available(iOS 17, *)) {
1762 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1763 [inputView firstRectForRange:singleRectRange]));
1765 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1770 if (@available(iOS 17, *)) {
1771 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1772 [inputView firstRectForRange:multiRectRange]));
1774 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1777 [inputView setTextInputState:@{@"text" : @"COM"}];
1779 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1782 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1784 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1786 [inputView setSelectionRects:@[
1793 if (@available(iOS 17, *)) {
1794 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1795 [inputView firstRectForRange:singleRectRange]));
1797 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1801 if (@available(iOS 17, *)) {
1802 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1803 [inputView firstRectForRange:multiRectRange]));
1805 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1808 [inputView setTextInputState:@{@"text" : @"COM"}];
1810 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1813 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1815 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1817 [inputView setSelectionRects:@[
1828 if (@available(iOS 17, *)) {
1829 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1830 [inputView firstRectForRange:singleRectRange]));
1832 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1837 if (@available(iOS 17, *)) {
1838 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1839 [inputView firstRectForRange:multiRectRange]));
1841 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1845 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1847 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1849 [inputView setSelectionRects:@[
1860 if (@available(iOS 17, *)) {
1861 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1862 [inputView firstRectForRange:singleRectRange]));
1864 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1868 if (@available(iOS 17, *)) {
1869 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1870 [inputView firstRectForRange:multiRectRange]));
1872 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1876 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1878 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1880 [inputView setSelectionRects:@[
1891 if (@available(iOS 17, *)) {
1892 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1893 [inputView firstRectForRange:multiRectRange]));
1895 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1899 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1901 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1903 [inputView setSelectionRects:@[
1914 if (@available(iOS 17, *)) {
1915 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1916 [inputView firstRectForRange:multiRectRange]));
1918 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1922 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1924 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1926 [inputView setSelectionRects:@[
1937 if (@available(iOS 17, *)) {
1938 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1939 [inputView firstRectForRange:multiRectRange]));
1941 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1945 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1947 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1949 [inputView setSelectionRects:@[
1960 if (@available(iOS 17, *)) {
1961 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1962 [inputView firstRectForRange:multiRectRange]));
1964 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1968 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1970 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1972 [inputView setSelectionRects:@[
1983 if (@available(iOS 17, *)) {
1984 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1985 [inputView firstRectForRange:multiRectRange]));
1987 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1991 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1993 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1995 [inputView setSelectionRects:@[
2006 if (@available(iOS 17, *)) {
2007 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
2008 [inputView firstRectForRange:multiRectRange]));
2010 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2014 - (void)testClosestPositionToPoint {
2016 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2019 [inputView setSelectionRects:@[
2024 CGPoint point = CGPointMake(150, 150);
2025 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2026 XCTAssertEqual(UITextStorageDirectionBackward,
2031 [inputView setSelectionRects:@[
2038 point = CGPointMake(125, 150);
2039 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2040 XCTAssertEqual(UITextStorageDirectionForward,
2045 [inputView setSelectionRects:@[
2052 point = CGPointMake(125, 201);
2053 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2054 XCTAssertEqual(UITextStorageDirectionBackward,
2058 [inputView setSelectionRects:@[
2064 point = CGPointMake(125, 250);
2065 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2066 XCTAssertEqual(UITextStorageDirectionBackward,
2070 [inputView setSelectionRects:@[
2075 point = CGPointMake(110, 50);
2076 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2077 XCTAssertEqual(UITextStorageDirectionForward,
2082 [inputView beginFloatingCursorAtPoint:CGPointZero];
2083 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2084 XCTAssertEqual(UITextStorageDirectionForward,
2086 [inputView endFloatingCursor];
2089 - (void)testClosestPositionToPointRTL {
2091 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2093 [inputView setSelectionRects:@[
2109 XCTAssertEqual(0U, position.
index);
2110 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2112 XCTAssertEqual(1U, position.
index);
2113 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2115 XCTAssertEqual(1U, position.
index);
2116 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2118 XCTAssertEqual(2U, position.
index);
2119 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2121 XCTAssertEqual(2U, position.
index);
2122 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2124 XCTAssertEqual(3U, position.
index);
2125 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2127 XCTAssertEqual(3U, position.
index);
2128 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2131 - (void)testSelectionRectsForRange {
2133 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2135 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2136 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2137 [inputView setSelectionRects:@[
2146 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2147 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2148 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2152 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2153 XCTAssertTrue(CGRectEqualToRect(
2154 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2155 [inputView selectionRectsForRange:range][0].rect));
2158 - (void)testClosestPositionToPointWithinRange {
2160 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2163 [inputView setSelectionRects:@[
2170 CGPoint point = CGPointMake(125, 150);
2173 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2175 UITextStorageDirectionForward,
2176 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2179 [inputView setSelectionRects:@[
2186 point = CGPointMake(125, 150);
2189 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2191 UITextStorageDirectionForward,
2192 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2195 - (void)testClosestPositionToPointWithPartialSelectionRects {
2197 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2204 XCTAssertTrue(CGRectEqualToRect(
2207 affinity:UITextStorageDirectionForward]],
2208 CGRectMake(100, 0, 0, 100)));
2211 XCTAssertTrue(CGRectEqualToRect(
2214 affinity:UITextStorageDirectionForward]],
2218 #pragma mark - Floating Cursor - Tests
2220 - (void)testFloatingCursorDoesNotThrow {
2223 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2224 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2225 [inputView endFloatingCursor];
2226 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2227 [inputView endFloatingCursor];
2230 - (void)testFloatingCursor {
2232 [inputView setTextInputState:@{
2234 @"selectionBase" : @1,
2235 @"selectionExtent" : @1,
2246 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2249 XCTAssertTrue(CGRectEqualToRect(
2252 affinity:UITextStorageDirectionForward]],
2253 CGRectMake(0, 0, 0, 100)));
2256 XCTAssertTrue(CGRectEqualToRect(
2259 affinity:UITextStorageDirectionForward]],
2260 CGRectMake(100, 100, 0, 100)));
2261 XCTAssertTrue(CGRectEqualToRect(
2264 affinity:UITextStorageDirectionForward]],
2265 CGRectMake(200, 200, 0, 100)));
2266 XCTAssertTrue(CGRectEqualToRect(
2269 affinity:UITextStorageDirectionForward]],
2270 CGRectMake(300, 300, 0, 100)));
2273 XCTAssertTrue(CGRectEqualToRect(
2276 affinity:UITextStorageDirectionForward]],
2277 CGRectMake(400, 300, 0, 100)));
2279 XCTAssertTrue(CGRectEqualToRect(
2282 affinity:UITextStorageDirectionForward]],
2286 [inputView setTextInputState:@{
2288 @"selectionBase" : @2,
2289 @"selectionExtent" : @2,
2292 XCTAssertTrue(CGRectEqualToRect(
2295 affinity:UITextStorageDirectionBackward]],
2296 CGRectMake(0, 0, 0, 100)));
2299 XCTAssertTrue(CGRectEqualToRect(
2302 affinity:UITextStorageDirectionBackward]],
2303 CGRectMake(100, 0, 0, 100)));
2304 XCTAssertTrue(CGRectEqualToRect(
2307 affinity:UITextStorageDirectionBackward]],
2308 CGRectMake(200, 100, 0, 100)));
2309 XCTAssertTrue(CGRectEqualToRect(
2312 affinity:UITextStorageDirectionBackward]],
2313 CGRectMake(300, 200, 0, 100)));
2314 XCTAssertTrue(CGRectEqualToRect(
2317 affinity:UITextStorageDirectionBackward]],
2318 CGRectMake(400, 300, 0, 100)));
2320 XCTAssertTrue(CGRectEqualToRect(
2323 affinity:UITextStorageDirectionBackward]],
2328 CGRect initialBounds = inputView.bounds;
2329 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2330 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2331 OCMVerify([
engine flutterTextInputView:inputView
2332 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2334 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2335 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2336 ([state[
@"Y"] isEqualToNumber:@(0)]);
2339 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2340 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2341 OCMVerify([
engine flutterTextInputView:inputView
2342 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2344 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2345 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2346 ([state[
@"Y"] isEqualToNumber:@(333)]);
2349 [inputView endFloatingCursor];
2350 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2351 OCMVerify([
engine flutterTextInputView:inputView
2352 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2354 withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2355 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2356 ([state[
@"Y"] isEqualToNumber:@(0)]);
2360 #pragma mark - UIKeyInput Overrides - Tests
2362 - (void)testInsertTextAddsPlaceholderSelectionRects {
2365 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2375 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2378 [inputView insertText:@"in"];
2406 #pragma mark - Autofill - Utilities
2408 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2411 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2412 @"keyboardAppearance" :
@"Brightness.light",
2413 @"obscureText" : @YES,
2414 @"inputAction" :
@"TextInputAction.unspecified",
2415 @"smartDashesType" :
@"0",
2416 @"smartQuotesType" :
@"0",
2417 @"autocorrect" : @YES
2421 return [_passwordTemplate mutableCopy];
2425 return [
self.installedInputViews
2426 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2429 - (void)commitAutofillContextAndVerify {
2433 [textInputPlugin handleMethodCall:methodCall
2434 result:^(id _Nullable result){
2437 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2442 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2446 #pragma mark - Autofill - Tests
2448 - (void)testDisablingAutofillOnInputClient {
2449 NSDictionary* config =
self.mutableTemplateCopy;
2450 [config setValue:@"YES" forKey:@"obscureText"];
2452 [
self setClientId:123 configuration:config];
2455 XCTAssertEqualObjects(inputView.textContentType,
@"");
2458 - (void)testAutofillEnabledByDefault {
2459 NSDictionary* config =
self.mutableTemplateCopy;
2460 [config setValue:@"NO" forKey:@"obscureText"];
2461 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2462 forKey:@"autofill"];
2464 [
self setClientId:123 configuration:config];
2467 XCTAssertNil(inputView.textContentType);
2470 - (void)testAutofillContext {
2471 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2474 @"uniqueIdentifier" : @"field1",
2475 @"hints" : @[ @"hint1" ],
2476 @"editingValue" : @{@"text" : @""}
2478 forKey:@"autofill"];
2480 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2482 @"uniqueIdentifier" : @"field2",
2483 @"hints" : @[ @"hint2" ],
2484 @"editingValue" : @{@"text" : @""}
2486 forKey:@"autofill"];
2488 NSMutableDictionary* config = [field1 mutableCopy];
2489 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2491 [
self setClientId:123 configuration:config];
2492 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2496 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2497 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2499 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2502 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2504 @"uniqueIdentifier" : @"field3",
2505 @"hints" : @[ @"hint3" ],
2506 @"editingValue" : @{@"text" : @""}
2508 forKey:@"autofill"];
2512 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2514 [
self setClientId:123 configuration:config];
2516 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2519 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2520 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2522 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2525 for (NSString* key in oldContext.allKeys) {
2526 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2530 config =
self.mutablePasswordTemplateCopy;
2533 [
self setClientId:124 configuration:config];
2534 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2536 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2539 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2540 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2543 for (NSString* key in oldContext.allKeys) {
2544 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2548 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2552 [
self setClientId:200 configuration:config];
2555 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2558 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2559 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2562 for (NSString* key in oldContext.allKeys) {
2563 XCTAssertEqual(oldContext[key],
textInputPlugin.autofillContext[key]);
2566 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2569 - (void)testCommitAutofillContext {
2570 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2572 @"uniqueIdentifier" : @"field1",
2573 @"hints" : @[ @"hint1" ],
2574 @"editingValue" : @{@"text" : @""}
2576 forKey:@"autofill"];
2578 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2580 @"uniqueIdentifier" : @"field2",
2581 @"hints" : @[ @"hint2" ],
2582 @"editingValue" : @{@"text" : @""}
2584 forKey:@"autofill"];
2586 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2588 @"uniqueIdentifier" : @"field3",
2589 @"hints" : @[ @"hint3" ],
2590 @"editingValue" : @{@"text" : @""}
2592 forKey:@"autofill"];
2594 NSMutableDictionary* config = [field1 mutableCopy];
2595 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2597 [
self setClientId:123 configuration:config];
2598 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2600 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2602 [
self commitAutofillContextAndVerify];
2603 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2606 [
self setClientId:123 configuration:config];
2608 [
self setClientId:124 configuration:field3];
2609 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2611 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2612 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2615 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2617 [
self commitAutofillContextAndVerify];
2618 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2621 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2623 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2627 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2628 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2630 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2632 [
self commitAutofillContextAndVerify];
2633 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2636 - (void)testAutofillInputViews {
2637 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2639 @"uniqueIdentifier" : @"field1",
2640 @"hints" : @[ @"hint1" ],
2641 @"editingValue" : @{@"text" : @""}
2643 forKey:@"autofill"];
2645 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2647 @"uniqueIdentifier" : @"field2",
2648 @"hints" : @[ @"hint2" ],
2649 @"editingValue" : @{@"text" : @""}
2651 forKey:@"autofill"];
2653 NSMutableDictionary* config = [field1 mutableCopy];
2654 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2656 [
self setClientId:123 configuration:config];
2657 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2660 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2663 XCTAssertEqual(inputFields.count, 2ul);
2664 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2669 withText:@"Autofilled!"];
2670 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2673 OCMVerify([
engine flutterTextInputView:inactiveView
2674 updateEditingClient:0
2675 withState:[OCMArg isNotNil]
2676 withTag:
@"field2"]);
2679 - (void)testPasswordAutofillHack {
2680 NSDictionary* config =
self.mutableTemplateCopy;
2681 [config setValue:@"YES" forKey:@"obscureText"];
2682 [
self setClientId:123 configuration:config];
2685 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2689 XCTAssert([inputView isKindOfClass:[UITextField
class]]);
2692 XCTAssertNotEqual([inputView performSelector:
@selector(font)], nil);
2695 - (void)testClearAutofillContextClearsSelection {
2696 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2697 NSDictionary* editingValue = @{
2698 @"text" :
@"REGULAR_TEXT_FIELD",
2699 @"composingBase" : @0,
2700 @"composingExtent" : @3,
2701 @"selectionBase" : @1,
2702 @"selectionExtent" : @4
2704 [regularField setValue:@{
2705 @"uniqueIdentifier" : @"field2",
2706 @"hints" : @[ @"hint2" ],
2707 @"editingValue" : editingValue,
2709 forKey:@"autofill"];
2710 [regularField addEntriesFromDictionary:editingValue];
2711 [
self setClientId:123 configuration:regularField];
2712 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2713 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2716 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2718 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(1, 3)));
2722 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2723 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2725 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2727 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2728 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2731 XCTAssert([oldInputView.text isEqualToString:
@""]);
2733 XCTAssert(NSEqualRanges(selectionRange.
range, NSMakeRange(0, 0)));
2736 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2738 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2739 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2741 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2744 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2745 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2747 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2749 [
self commitAutofillContextAndVerify];
2752 - (void)testScribbleSetSelectionRects {
2753 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2754 NSDictionary* editingValue = @{
2755 @"text" :
@"REGULAR_TEXT_FIELD",
2756 @"composingBase" : @0,
2757 @"composingExtent" : @3,
2758 @"selectionBase" : @1,
2759 @"selectionExtent" : @4
2761 [regularField setValue:@{
2762 @"uniqueIdentifier" : @"field1",
2763 @"hints" : @[ @"hint2" ],
2764 @"editingValue" : editingValue,
2766 forKey:@"autofill"];
2767 [regularField addEntriesFromDictionary:editingValue];
2768 [
self setClientId:123 configuration:regularField];
2769 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2770 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2772 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2773 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2777 [textInputPlugin handleMethodCall:methodCall
2778 result:^(id _Nullable result){
2781 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2784 - (void)testDecommissionedViewAreNotReusedByAutofill {
2786 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
2787 [configuration setValue:@{
2788 @"uniqueIdentifier" : @"field1",
2789 @"hints" : @[ UITextContentTypePassword ],
2790 @"editingValue" : @{@"text" : @""}
2792 forKey:@"autofill"];
2793 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2795 [
self setClientId:123 configuration:configuration];
2797 [
self setTextInputHide];
2800 [
self setClientId:124 configuration:configuration];
2804 XCTAssertNotNil(previousActiveView);
2808 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2815 #pragma mark - Accessibility - Tests
2817 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2818 [
self setClientId:123 configuration:self.mutableTemplateCopy];
2821 [
self setTextInputShow];
2823 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2826 XCTAssertEqual([inputFields count], 1u);
2829 [
self setTextInputHide];
2831 inputFields =
self.installedInputViews;
2834 XCTAssertEqual([inputFields count], 0u);
2837 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2840 UIView* container = [[UIView alloc] init];
2841 UIAccessibilityElement* backing =
2842 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2843 inputView.backingTextInputAccessibilityObject = backing;
2845 inputView.isAccessibilityFocused = YES;
2846 [inputView accessibilityElementDidBecomeFocused];
2848 XCTAssertEqual(inputView.receivedNotification, UIAccessibilityScreenChangedNotification);
2849 XCTAssertEqual(inputView.receivedNotificationTarget, backing);
2852 - (void)testFlutterTokenizerCanParseLines {
2854 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2857 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2858 XCTAssertEqual(range.
range.location, 0u);
2859 XCTAssertEqual(range.
range.length, 0u);
2861 [inputView insertText:@"how are you\nI am fine, Thank you"];
2863 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
2864 XCTAssertEqual(range.
range.location, 0u);
2865 XCTAssertEqual(range.
range.length, 11u);
2867 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
2868 XCTAssertEqual(range.
range.location, 0u);
2869 XCTAssertEqual(range.
range.length, 11u);
2871 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
2872 XCTAssertEqual(range.
range.location, 0u);
2873 XCTAssertEqual(range.
range.length, 11u);
2875 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
2876 XCTAssertEqual(range.
range.location, 12u);
2877 XCTAssertEqual(range.
range.length, 20u);
2879 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
2880 XCTAssertEqual(range.
range.location, 12u);
2881 XCTAssertEqual(range.
range.length, 20u);
2883 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
2884 XCTAssertEqual(range.
range.location, 12u);
2885 XCTAssertEqual(range.
range.length, 20u);
2888 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
2890 [inputView insertText:@"0123456789\n012345"];
2891 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2894 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2895 withGranularity:UITextGranularityLine
2896 inDirection:UITextStorageDirectionBackward];
2897 XCTAssertEqual(range.
range.location, 11u);
2898 XCTAssertEqual(range.
range.length, 6u);
2901 - (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
2903 [inputView insertText:@"0123456789\n012345"];
2904 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2907 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
2908 withGranularity:UITextGranularityLine
2909 inDirection:UITextStorageDirectionForward];
2910 if (@available(iOS 17.0, *)) {
2911 XCTAssertNil(range);
2913 XCTAssertEqual(range.
range.location, 11u);
2914 XCTAssertEqual(range.
range.length, 6u);
2918 - (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
2920 [inputView insertText:@"0123456789\n012345"];
2921 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2926 withGranularity:UITextGranularityLine
2927 inDirection:UITextStorageDirectionForward];
2928 if (@available(iOS 17.0, *)) {
2929 XCTAssertNil(range);
2931 XCTAssertEqual(range.
range.location, 0u);
2932 XCTAssertEqual(range.
range.length, 0u);
2936 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2941 __weak UIView* activeView;
2946 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2949 result:^(id _Nullable result){
2955 result:^(id _Nullable result){
2957 XCTAssertNotNil(activeView);
2960 XCTAssertNotNil(activeView);
2963 - (void)testFlutterTextInputPluginHostViewNilCrash {
2966 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
2969 - (void)testFlutterTextInputPluginHostViewNotNil {
2975 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2978 - (void)testSetPlatformViewClient {
2985 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2987 result:^(id _Nullable result){
2990 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
2993 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2995 result:^(id _Nullable result){
2997 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
3000 - (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
3001 if (@available(iOS 16.0, *)) {
3003 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3004 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
3005 @"editMenuInteraction setup delegate correctly");
3009 - (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
3010 if (@available(iOS 16.0, *)) {
3014 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
3018 - (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
3019 if (@available(iOS 16.0, *)) {
3024 [myViewController loadView];
3027 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3029 result:^(id _Nullable result){
3035 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3037 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3038 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3040 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3041 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3042 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3043 .andDo(^(NSInvocation* invocation) {
3045 [invocation retainArguments];
3046 UIEditMenuConfiguration* config;
3047 [invocation getArgument:&config atIndex:2];
3048 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
3049 @"UIEditMenuConfiguration must use automatic arrow direction.");
3050 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
3051 @"UIEditMenuConfiguration must have the correct point.");
3052 [expectation fulfill];
3055 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3056 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
3058 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3059 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3060 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3064 - (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
3065 if (@available(iOS 16.0, *)) {
3070 [myViewController loadView];
3074 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3076 result:^(id _Nullable result){
3082 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3084 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3085 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3087 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3088 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3089 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3090 .andDo(^(NSInvocation* invocation) {
3091 [expectation fulfill];
3094 myInputView.frame = CGRectMake(10, 20, 30, 40);
3095 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3096 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3098 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3099 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3100 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3103 [myInputView editMenuInteraction:mockInteraction
3104 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3106 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3107 @"targetRectForConfiguration must return the correct target rect.");
3111 - (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3112 if (@available(iOS 16.0, *)) {
3117 [myViewController loadView];
3121 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3123 result:^(id _Nullable result){
3129 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3131 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3132 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3134 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3135 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3136 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3137 .andDo(^(NSInvocation* invocation) {
3138 [expectation fulfill];
3141 myInputView.frame = CGRectMake(10, 20, 30, 40);
3142 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3143 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3145 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3146 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3147 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3149 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3151 action:@selector(copy:)
3153 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3155 action:@selector(paste:)
3157 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3159 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3160 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3161 suggestedActions:suggestedActions];
3162 XCTAssertEqualObjects(menu.children, suggestedActions,
3163 @"Must show suggested items by default.");
3167 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3168 if (@available(iOS 16.0, *)) {
3173 [myViewController loadView];
3177 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3179 result:^(id _Nullable result){
3185 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3187 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3188 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3190 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3191 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3192 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3193 .andDo(^(NSInvocation* invocation) {
3194 [expectation fulfill];
3197 myInputView.frame = CGRectMake(10, 20, 30, 40);
3198 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3199 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3201 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3202 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3204 BOOL shownEditMenu =
3205 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3206 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3207 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3209 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3211 action:@selector(copy:)
3213 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3215 action:@selector(paste:)
3217 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3219 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3220 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3221 suggestedActions:suggestedActions];
3223 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3224 XCTAssertEqualObjects(menu.children, expectedChildren);
3228 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3229 if (@available(iOS 16.0, *)) {
3234 [myViewController loadView];
3238 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3240 result:^(id _Nullable result){
3246 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3248 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3249 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3251 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3252 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3253 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3254 .andDo(^(NSInvocation* invocation) {
3255 [expectation fulfill];
3258 myInputView.frame = CGRectMake(10, 20, 30, 40);
3259 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3260 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3262 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3263 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3265 BOOL shownEditMenu =
3266 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3267 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3268 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3270 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3272 action:@selector(copy:)
3274 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3276 action:@selector(cut:)
3278 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3280 action:@selector(paste:)
3293 NSArray<UIMenuElement*>* suggestedActions = @[
3294 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3295 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3298 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3299 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3300 suggestedActions:suggestedActions];
3302 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3303 XCTAssertEqualObjects(menu.children, expectedActions);
3307 - (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3308 if (@available(iOS 16.0, *)) {
3313 [myViewController loadView];
3317 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3319 result:^(id _Nullable result){
3325 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3327 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3328 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3330 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3331 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3332 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3333 .andDo(^(NSInvocation* invocation) {
3334 [expectation fulfill];
3337 myInputView.frame = CGRectMake(10, 20, 30, 40);
3338 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3339 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3341 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3342 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3343 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3346 BOOL shownEditMenu =
3347 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3348 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3349 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3351 NSArray<UICommand*>* suggestedActions = @[
3352 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3355 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3356 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3357 suggestedActions:suggestedActions];
3358 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3360 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3361 @"Must create search web item in the tree.");
3362 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3363 @"Must create look up item in the tree.");
3364 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3365 @"Must create share item in the tree.");
3369 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3371 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3373 [inputView setTextInputClient:123];
3374 [inputView reloadInputViews];
3375 [inputView becomeFirstResponder];
3376 XCTAssert(inputView.isFirstResponder);
3378 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3379 [NSNotificationCenter.defaultCenter
3380 postNotificationName:UIKeyboardWillShowNotification
3382 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3386 [textInputPlugin handleMethodCall:onPointerMoveCall
3387 result:^(id _Nullable result){
3389 XCTAssertFalse(inputView.isFirstResponder);
3393 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3394 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3395 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3396 UIScene* scene = scenes.anyObject;
3397 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3398 UIWindowScene* windowScene = (UIWindowScene*)scene;
3399 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3400 UIWindow* window = windowScene.windows[0];
3401 [window addSubview:viewController.view];
3403 [viewController loadView];
3406 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3408 [inputView setTextInputClient:123];
3409 [inputView reloadInputViews];
3410 [inputView becomeFirstResponder];
3413 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3414 [subView removeFromSuperview];
3418 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3419 [NSNotificationCenter.defaultCenter
3420 postNotificationName:UIKeyboardWillShowNotification
3422 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3426 [textInputPlugin handleMethodCall:onPointerMoveCall
3427 result:^(id _Nullable result){
3430 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3431 [subView removeFromSuperview];
3436 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3437 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3438 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3439 UIScene* scene = scenes.anyObject;
3440 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3441 UIWindowScene* windowScene = (UIWindowScene*)scene;
3442 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3443 UIWindow* window = windowScene.windows[0];
3444 [window addSubview:viewController.view];
3446 [viewController loadView];
3449 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3451 [inputView setTextInputClient:123];
3452 [inputView reloadInputViews];
3453 [inputView becomeFirstResponder];
3455 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3456 [NSNotificationCenter.defaultCenter
3457 postNotificationName:UIKeyboardWillShowNotification
3459 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3463 [textInputPlugin handleMethodCall:onPointerMoveCall
3464 result:^(id _Nullable result){
3468 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3473 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3474 result:^(id _Nullable result){
3478 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3480 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3481 [subView removeFromSuperview];
3486 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3487 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3488 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3489 UIScene* scene = scenes.anyObject;
3490 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3491 UIWindowScene* windowScene = (UIWindowScene*)scene;
3492 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3493 UIWindow* window = windowScene.windows[0];
3494 [window addSubview:viewController.view];
3496 [viewController loadView];
3499 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3501 [inputView setTextInputClient:123];
3502 [inputView reloadInputViews];
3503 [inputView becomeFirstResponder];
3505 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3506 [NSNotificationCenter.defaultCenter
3507 postNotificationName:UIKeyboardWillShowNotification
3509 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3513 [textInputPlugin handleMethodCall:onPointerMoveCall
3514 result:^(id _Nullable result){
3517 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3522 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3523 result:^(id _Nullable result){
3526 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3531 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3532 result:^(id _Nullable result){
3535 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3536 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3537 [subView removeFromSuperview];
3542 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
3544 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3545 [inputView setTextInputClient:123];
3546 [inputView reloadInputViews];
3547 [inputView becomeFirstResponder];
3549 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3550 XCTAssertEqualObjects(inputView, firstResponder);
3554 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3561 [subInputView addSubview:subFirstResponderInputView];
3562 [inputView addSubview:subInputView];
3563 [inputView addSubview:otherSubInputView];
3564 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3565 [inputView setTextInputClient:123];
3566 [inputView reloadInputViews];
3567 [subInputView setTextInputClient:123];
3568 [subInputView reloadInputViews];
3569 [otherSubInputView setTextInputClient:123];
3570 [otherSubInputView reloadInputViews];
3571 [subFirstResponderInputView setTextInputClient:123];
3572 [subFirstResponderInputView reloadInputViews];
3573 [subFirstResponderInputView becomeFirstResponder];
3575 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3576 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3580 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3582 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3583 [inputView setTextInputClient:123];
3584 [inputView reloadInputViews];
3586 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3587 XCTAssertNil(firstResponder);
3591 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3592 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3593 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3594 UIScene* scene = scenes.anyObject;
3595 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3596 UIWindowScene* windowScene = (UIWindowScene*)scene;
3597 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3598 UIWindow* window = windowScene.windows[0];
3599 [window addSubview:viewController.view];
3601 [viewController loadView];
3603 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3604 initWithDescription:
3605 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3606 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3607 .andDo(^(NSInvocation* invocation) {
3608 [expectation fulfill];
3610 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3611 [NSNotificationCenter.defaultCenter
3612 postNotificationName:UIKeyboardWillShowNotification
3614 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3618 [textInputPlugin handleMethodCall:initialMoveCall
3619 result:^(id _Nullable result){
3624 [textInputPlugin handleMethodCall:subsequentMoveCall
3625 result:^(id _Nullable result){
3631 [textInputPlugin handleMethodCall:pointerUpCall
3632 result:^(id _Nullable result){
3635 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3639 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3640 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3641 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3642 UIScene* scene = scenes.anyObject;
3643 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3644 UIWindowScene* windowScene = (UIWindowScene*)scene;
3645 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3646 UIWindow* window = windowScene.windows[0];
3647 [window addSubview:viewController.view];
3649 [viewController loadView];
3651 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3652 [NSNotificationCenter.defaultCenter
3653 postNotificationName:UIKeyboardWillShowNotification
3655 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3659 [textInputPlugin handleMethodCall:initialMoveCall
3660 result:^(id _Nullable result){
3665 [textInputPlugin handleMethodCall:subsequentMoveCall
3666 result:^(id _Nullable result){
3672 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3673 result:^(id _Nullable result){
3679 [textInputPlugin handleMethodCall:pointerUpCall
3680 result:^(id _Nullable result){
3682 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3683 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3685 XCTNSPredicateExpectation* expectation =
3686 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3687 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3691 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3692 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3693 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3694 UIScene* scene = scenes.anyObject;
3695 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3696 UIWindowScene* windowScene = (UIWindowScene*)scene;
3697 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3698 UIWindow* window = windowScene.windows[0];
3699 [window addSubview:viewController.view];
3701 [viewController loadView];
3704 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3706 [inputView setTextInputClient:123];
3707 [inputView reloadInputViews];
3708 [inputView becomeFirstResponder];
3710 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3711 [NSNotificationCenter.defaultCenter
3712 postNotificationName:UIKeyboardWillShowNotification
3714 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3718 [textInputPlugin handleMethodCall:initialMoveCall
3719 result:^(id _Nullable result){
3724 [textInputPlugin handleMethodCall:subsequentMoveCall
3725 result:^(id _Nullable result){
3731 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3732 result:^(id _Nullable result){
3738 [textInputPlugin handleMethodCall:pointerUpCall
3739 result:^(id _Nullable result){
3741 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3742 return textInputPlugin.cachedFirstResponder.isFirstResponder;
3744 XCTNSPredicateExpectation* expectation =
3745 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3746 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3750 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3751 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3752 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3753 UIScene* scene = scenes.anyObject;
3754 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3755 UIWindowScene* windowScene = (UIWindowScene*)scene;
3756 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3757 UIWindow* window = windowScene.windows[0];
3758 [window addSubview:viewController.view];
3760 [viewController loadView];
3762 XCTestExpectation* expectation =
3763 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3764 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3765 [NSNotificationCenter.defaultCenter
3766 postNotificationName:UIKeyboardWillShowNotification
3768 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3772 [textInputPlugin handleMethodCall:initialMoveCall
3773 result:^(id _Nullable result){
3778 [textInputPlugin handleMethodCall:subsequentMoveCall
3779 result:^(id _Nullable result){
3784 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3785 result:^(id _Nullable result){
3792 handleMethodCall:pointerUpCall
3793 result:^(id _Nullable result) {
3794 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3795 viewController.flutterScreenIfViewLoaded.bounds.size.height -
3796 keyboardFrame.origin.y);
3797 [expectation fulfill];
3802 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3803 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3804 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3805 UIScene* scene = scenes.anyObject;
3806 XCTAssert([scene isKindOfClass:[UIWindowScene
class]],
@"Must be a window scene for test");
3807 UIWindowScene* windowScene = (UIWindowScene*)scene;
3808 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3809 UIWindow* window = windowScene.windows[0];
3810 [window addSubview:viewController.view];
3812 [viewController loadView];
3814 XCTestExpectation* expectation =
3815 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3816 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3817 [NSNotificationCenter.defaultCenter
3818 postNotificationName:UIKeyboardWillShowNotification
3820 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3824 [textInputPlugin handleMethodCall:initialMoveCall
3825 result:^(id _Nullable result){
3830 [textInputPlugin handleMethodCall:subsequentMoveCall
3831 result:^(id _Nullable result){
3838 handleMethodCall:pointerUpCall
3839 result:^(id _Nullable result) {
3840 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3841 viewController.flutterScreenIfViewLoaded.bounds.size.height);
3842 [expectation fulfill];
3846 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3847 [UIView setAnimationsEnabled:YES];
3848 [textInputPlugin showKeyboardAndRemoveScreenshot];
3850 UIView.areAnimationsEnabled,
3851 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3854 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3855 [UIView setAnimationsEnabled:YES];
3856 [textInputPlugin showKeyboardAndRemoveScreenshot];
3858 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3860 return UIView.areAnimationsEnabled;
3862 XCTNSPredicateExpectation* expectation =
3863 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3864 [
self waitForExpectations:@[ expectation ] timeout:10.0];
NSArray< FlutterTextSelectionRect * > * selectionRects
UITextRange * markedTextRange
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
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)