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;
31- (
id<FlutterTextInputDelegate>)textInputDelegate;
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 {
55- (
BOOL)accessibilityElementIsFocused {
56 return _isAccessibilityFocused;
62@property(nonatomic, strong) UITextField*
textField;
67@property(nonatomic, readonly) UIView* inputHider;
68@property(nonatomic, readonly) UIView* keyboardViewContainer;
69@property(nonatomic, readonly) UIView* keyboardView;
70@property(nonatomic, assign) UIView* cachedFirstResponder;
71@property(nonatomic, readonly) CGRect keyboardRect;
72@property(nonatomic, readonly)
BOOL pendingAutofillRemoval;
73@property(nonatomic, readonly)
BOOL pendingInputViewRemoval;
74@property(nonatomic, readonly)
75 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
77- (void)cleanUpViewHierarchy:(
BOOL)includeActiveView
78 clearText:(
BOOL)clearText
79 delayRemoval:(
BOOL)delayRemoval;
80- (NSArray<UIView*>*)textInputViews;
83- (void)startLiveTextInput;
84- (void)showKeyboardAndRemoveScreenshot;
90class MockPlatformViewDelegate :
public PlatformView::Delegate {
92 void OnPlatformViewCreated(std::unique_ptr<Surface>
surface)
override {}
93 void OnPlatformViewDestroyed()
override {}
94 void OnPlatformViewScheduleFrame()
override {}
95 void OnPlatformViewAddView(int64_t
view_id,
96 const ViewportMetrics& viewport_metrics,
97 AddViewCallback
callback)
override {}
98 void OnPlatformViewRemoveView(int64_t
view_id, RemoveViewCallback
callback)
override {}
99 void OnPlatformViewSendViewFocusEvent(
const ViewFocusEvent& event)
override {};
100 void OnPlatformViewSetNextFrameCallback(
const fml::closure& closure)
override {}
101 void OnPlatformViewSetViewportMetrics(int64_t
view_id,
const ViewportMetrics& metrics)
override {}
103 void OnPlatformViewDispatchPlatformMessage(std::unique_ptr<PlatformMessage>
message)
override {}
104 void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr<PointerDataPacket> packet)
override {
107 return {.has_platform_view =
false};
109 void OnPlatformViewDispatchSemanticsAction(int64_t
view_id,
113 void OnPlatformViewSetSemanticsEnabled(
bool enabled)
override {}
114 void OnPlatformViewSetAccessibilityFeatures(int32_t flags)
override {}
115 void OnPlatformViewRegisterTexture(std::shared_ptr<Texture>
texture)
override {}
116 void OnPlatformViewUnregisterTexture(int64_t
texture_id)
override {}
117 void OnPlatformViewMarkTextureFrameAvailable(int64_t
texture_id)
override {}
119 void LoadDartDeferredLibrary(intptr_t loading_unit_id,
120 std::unique_ptr<const fml::Mapping> snapshot_data,
121 std::unique_ptr<const fml::Mapping> snapshot_instructions)
override {
123 void LoadDartDeferredLibraryError(intptr_t loading_unit_id,
124 const std::string error_message,
125 bool transient)
override {}
126 void UpdateAssetResolverByType(std::unique_ptr<flutter::AssetResolver> updated_asset_resolver,
139 NSDictionary* _template;
157 UIPasteboard.generalPasteboard.items = @[];
163 [textInputPlugin.autofillContext removeAllObjects];
164 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
165 [[[[textInputPlugin textInputView] superview] subviews]
166 makeObjectsPerformSelector:@selector(removeFromSuperview)];
171- (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
174 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
175 [textInputPlugin handleMethodCall:setClientCall
176 result:^(id _Nullable result){
180- (void)setClientClear {
183 [textInputPlugin handleMethodCall:clearClientCall
184 result:^(id _Nullable result){
188- (void)setTextInputShow {
191 [textInputPlugin handleMethodCall:setClientCall
192 result:^(id _Nullable result){
196- (void)setTextInputHide {
199 [textInputPlugin handleMethodCall:setClientCall
200 result:^(id _Nullable result){
204- (void)flushScheduledAsyncBlocks {
205 __block
bool done =
false;
206 XCTestExpectation* expectation =
207 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
208 dispatch_async(dispatch_get_main_queue(), ^{
211 dispatch_async(dispatch_get_main_queue(), ^{
213 [expectation fulfill];
215 [
self waitForExpectations:@[ expectation ] timeout:10];
218- (NSMutableDictionary*)mutableTemplateCopy {
221 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
222 @"keyboardAppearance" :
@"Brightness.light",
223 @"obscureText" : @NO,
224 @"inputAction" :
@"TextInputAction.unspecified",
225 @"smartDashesType" :
@"0",
226 @"smartQuotesType" :
@"0",
227 @"autocorrect" : @YES,
228 @"enableInteractiveSelection" : @YES,
232 return [_template mutableCopy];
236 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
237 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
241- (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
242 atIndex:(NSInteger)index {
245 withGranularity:UITextGranularityLine
246 inDirection:UITextLayoutDirectionRight];
251- (void)updateConfig:(NSDictionary*)config {
254 [textInputPlugin handleMethodCall:updateConfigCall
255 result:^(id _Nullable result){
261- (void)testWillNotCrashWhenViewControllerIsNil {
268 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
271 result:^(id _Nullable result) {
272 XCTAssertNil(result);
273 [expectation fulfill];
275 XCTAssertNil(inputPlugin.activeView);
276 [
self waitForExpectations:@[ expectation ] timeout:1.0];
279- (void)testInvokeStartLiveTextInput {
284 result:^(id _Nullable result){
286 OCMVerify([mockPlugin startLiveTextInput]);
289- (void)testNoDanglingEnginePointer {
299 weakFlutterEngine = flutterEngine;
300 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
302 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
303 weakFlutterTextInputPlugin = flutterTextInputPlugin;
307 NSDictionary* config =
self.mutableTemplateCopy;
310 arguments:@[ [NSNumber numberWithInt:123], config ]];
312 result:^(id _Nullable result){
314 currentView = flutterTextInputPlugin.activeView;
317 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
318 XCTAssertNotNil(currentView,
@"current view must not be nil");
320 XCTAssertNil(weakFlutterTextInputPlugin);
323 XCTAssertNil(currentView.textInputDelegate);
326- (void)testSecureInput {
327 NSDictionary* config =
self.mutableTemplateCopy;
328 [config setValue:@"YES" forKey:@"obscureText"];
329 [
self setClientId:123 configuration:config];
332 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
339 XCTAssertTrue(inputView.secureTextEntry);
342 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
345 XCTAssertEqual(inputFields.count, 1ul);
353 XCTAssert(inputView.autofillId.length > 0);
356- (void)testKeyboardType {
357 NSDictionary* config =
self.mutableTemplateCopy;
358 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
359 [
self setClientId:123 configuration:config];
362 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
367 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
370- (void)testKeyboardTypeWebSearch {
371 NSDictionary* config =
self.mutableTemplateCopy;
372 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
373 [
self setClientId:123 configuration:config];
376 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
381 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
384- (void)testKeyboardTypeTwitter {
385 NSDictionary* config =
self.mutableTemplateCopy;
386 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
387 [
self setClientId:123 configuration:config];
390 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
395 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
398- (void)testVisiblePasswordUseAlphanumeric {
399 NSDictionary* config =
self.mutableTemplateCopy;
400 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
401 [
self setClientId:123 configuration:config];
404 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
409 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
412- (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
413 NSDictionary* config =
self.mutableTemplateCopy;
414 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
415 [
self setClientId:123 configuration:config];
420 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
421 [
self setClientId:124 configuration:config];
426- (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
430 if (@available(iOS 17.0, *)) {
432 OCMVerify(never(), [
engine flutterTextInputView:inputView
433 showAutocorrectionPromptRectForStart:0
437 OCMVerify([
engine flutterTextInputView:inputView
438 showAutocorrectionPromptRectForStart:0
444- (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
446 __block
int updateCount = 0;
447 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
448 .andDo(^(NSInvocation* invocation) {
452 [inputView.text setString:@"Some initial text"];
453 XCTAssertEqual(updateCount, 0);
456 [inputView setSelectedTextRange:textRange];
457 XCTAssertEqual(updateCount, 1);
460 NSDictionary* config =
self.mutableTemplateCopy;
461 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
462 [config setValue:@(NO) forKey:@"obscureText"];
463 [config setValue:@(NO) forKey:@"enableDeltaModel"];
464 [inputView configureWithDictionary:config];
467 [inputView setSelectedTextRange:textRange];
469 XCTAssertEqual(updateCount, 1);
472- (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
474 if (@available(iOS 17.0, *)) {
478 if (@available(iOS 14.0, *)) {
481 __block
int callCount = 0;
482 OCMStub([
engine flutterTextInputView:inputView
483 showAutocorrectionPromptRectForStart:0
486 .andDo(^(NSInvocation* invocation) {
492 XCTAssertEqual(callCount, 1);
494 UIScribbleInteraction* scribbleInteraction =
495 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
497 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
501 XCTAssertEqual(callCount, 1);
503 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
504 [inputView resetScribbleInteractionStatusIfEnding];
507 XCTAssertEqual(callCount, 2);
509 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
513 XCTAssertEqual(callCount, 2);
515 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
519 XCTAssertEqual(callCount, 2);
521 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
524 XCTAssertEqual(callCount, 3);
528- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
534 arguments:@[ @(123),
self.mutableTemplateCopy ]];
536 result:^(id _Nullable result){
543 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
547 arguments:@{@"transform" : yOffsetMatrix}];
549 result:^(id _Nullable result){
552 if (@available(iOS 17, *)) {
553 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
554 @"The input hider should overlap with the text on and after iOS 17");
557 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
558 @"The input hider should be on the origin of screen on and before iOS 16.");
562- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
568 toPosition:toPosition];
569 NSRange range = flutterRange.
range;
571 XCTAssertEqual(range.location, 0ul);
572 XCTAssertEqual(range.length, 2ul);
575- (void)testTextInRange {
576 NSDictionary* config =
self.mutableTemplateCopy;
577 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
578 [
self setClientId:123 configuration:config];
579 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
582 [inputView insertText:@"test"];
585 NSString* substring = [inputView textInRange:range];
586 XCTAssertEqual(substring.length, 4ul);
589 substring = [inputView textInRange:range];
590 XCTAssertEqual(substring.length, 0ul);
593- (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
594 NSDictionary* config =
self.mutableTemplateCopy;
595 [
self setClientId:123 configuration:config];
596 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
599 [inputView insertText:@"text"];
602 NSString* substring = [inputView textInRange:range];
603 XCTAssertNil(substring);
606- (void)testStandardEditActions {
607 NSDictionary* config =
self.mutableTemplateCopy;
608 [
self setClientId:123 configuration:config];
609 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
612 [inputView insertText:@"aaaa"];
613 [inputView selectAll:nil];
615 [inputView insertText:@"bbbb"];
616 XCTAssertTrue([inputView canPerformAction:@selector(paste:) withSender:nil]);
617 [inputView paste:nil];
618 [inputView selectAll:nil];
619 [inputView copy:nil];
620 [inputView paste:nil];
621 [inputView selectAll:nil];
622 [inputView delete:nil];
623 [inputView paste:nil];
624 [inputView paste:nil];
627 NSString* substring = [inputView textInRange:range];
628 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
631- (void)testCanPerformActionForSelectActions {
632 NSDictionary* config =
self.mutableTemplateCopy;
633 [
self setClientId:123 configuration:config];
634 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
637 XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
639 [inputView insertText:@"aaaa"];
641 XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
644- (void)testCanPerformActionCaptureTextFromCamera {
645 if (@available(iOS 15.0, *)) {
646 NSDictionary* config =
self.mutableTemplateCopy;
647 [
self setClientId:123 configuration:config];
648 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
651 [inputView becomeFirstResponder];
652 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
654 [inputView insertText:@"test"];
655 [inputView selectAll:nil];
656 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
660- (void)testDeletingBackward {
661 NSDictionary* config =
self.mutableTemplateCopy;
662 [
self setClientId:123 configuration:config];
663 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
666 [inputView insertText:@"ឹ😀 text 🥰👨👩👧👦🇺🇳ดี "];
667 [inputView deleteBackward];
668 [inputView deleteBackward];
671 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
672 [inputView deleteBackward];
673 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
674 [inputView deleteBackward];
675 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
676 [inputView deleteBackward];
677 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
678 [inputView deleteBackward];
680 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
681 [inputView deleteBackward];
682 [inputView deleteBackward];
683 [inputView deleteBackward];
684 [inputView deleteBackward];
685 [inputView deleteBackward];
686 [inputView deleteBackward];
688 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
689 [inputView deleteBackward];
690 XCTAssertEqualObjects(inputView.text,
@"ឹ");
691 [inputView deleteBackward];
692 XCTAssertEqualObjects(inputView.text,
@"");
697- (void)testSystemOnlyAddingPartialComposedCharacter {
698 NSDictionary* config =
self.mutableTemplateCopy;
699 [
self setClientId:123 configuration:config];
700 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
703 [inputView insertText:@"👨👩👧👦"];
704 [inputView deleteBackward];
707 [inputView insertText:[@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)]];
708 [inputView insertText:@"아"];
710 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
713 [inputView deleteBackward];
716 [inputView insertText:@"😀"];
717 [inputView deleteBackward];
719 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
720 [inputView insertText:@"아"];
721 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
724 [inputView deleteBackward];
727 [inputView deleteBackward];
729 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
730 [inputView insertText:@"아"];
732 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
735- (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
736 NSDictionary* config =
self.mutableTemplateCopy;
737 [
self setClientId:123 configuration:config];
738 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
741 [inputView insertText:@"👨👩👧👦"];
742 [inputView deleteBackward];
743 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
746 NSString* brokenEmoji = [@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)];
747 [inputView insertText:brokenEmoji];
748 [inputView insertText:@"아"];
750 NSString* finalText = [NSString stringWithFormat:@"%@아", brokenEmoji];
751 XCTAssertEqualObjects(inputView.text, finalText);
754- (void)testPastingNonTextDisallowed {
755 NSDictionary* config =
self.mutableTemplateCopy;
756 [
self setClientId:123 configuration:config];
757 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
760 UIPasteboard.generalPasteboard.color = UIColor.redColor;
761 XCTAssertNil(UIPasteboard.generalPasteboard.string);
762 XCTAssertFalse([inputView canPerformAction:@selector(paste:) withSender:nil]);
763 [inputView paste:nil];
765 XCTAssertEqualObjects(inputView.text,
@"");
768- (void)testNoZombies {
775 [passwordView.textField description];
777 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
780- (void)testInputViewCrash {
785 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
786 activeView = inputPlugin.activeView;
788 [activeView updateEditingState];
791- (void)testDoNotReuseInputViews {
792 NSDictionary* config =
self.mutableTemplateCopy;
793 [
self setClientId:123 configuration:config];
795 [
self setClientId:456 configuration:config];
797 XCTAssertNotNil(currentView);
802- (void)ensureOnlyActiveViewCanBecomeFirstResponder {
804 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
808- (void)testPropagatePressEventsToViewController {
810 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
811 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
815 NSDictionary* config =
self.mutableTemplateCopy;
816 [
self setClientId:123 configuration:config];
818 [
self setTextInputShow];
820 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
821 withEvent:OCMClassMock([UIPressesEvent class])];
823 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
824 withEvent:[OCMArg isNotNil]]);
825 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
826 withEvent:[OCMArg isNotNil]]);
828 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
829 withEvent:OCMClassMock([UIPressesEvent class])];
831 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
832 withEvent:[OCMArg isNotNil]]);
833 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
834 withEvent:[OCMArg isNotNil]]);
837- (void)testPropagatePressEventsToViewController2 {
839 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
840 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
844 NSDictionary* config =
self.mutableTemplateCopy;
845 [
self setClientId:123 configuration:config];
846 [
self setTextInputShow];
849 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
850 withEvent:OCMClassMock([UIPressesEvent class])];
852 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
853 withEvent:[OCMArg isNotNil]]);
854 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
855 withEvent:[OCMArg isNotNil]]);
858 [
self setClientId:321 configuration:config];
859 [
self setTextInputShow];
861 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
863 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
864 withEvent:OCMClassMock([UIPressesEvent class])];
866 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
867 withEvent:[OCMArg isNotNil]]);
868 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
869 withEvent:[OCMArg isNotNil]]);
872- (void)testHotRestart {
873 flutter::MockPlatformViewDelegate mock_platform_view_delegate;
874 auto thread = std::make_unique<fml::Thread>(
"TextInputHotRestart");
875 auto thread_task_runner = thread->GetTaskRunner();
881 id mockFlutterView = OCMClassMock([
FlutterView class]);
884 OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
885 OCMStub([mockFlutterViewController
textInputPlugin]).andReturn(mockFlutterTextInputPlugin);
888 thread_task_runner->PostTask([&] {
889 auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
890 mock_platform_view_delegate,
891 mock_platform_view_delegate.settings_.enable_impeller
897 std::make_shared<
fml::SyncSwitch>());
899 platform_view->SetOwnerViewController(mockFlutterViewController);
901 OCMExpect([mockFlutterTextInputPlugin reset]);
903 OCMVerifyAll(mockFlutterView);
910- (void)testUpdateSecureTextEntry {
911 NSDictionary* config =
self.mutableTemplateCopy;
912 [config setValue:@"YES" forKey:@"obscureText"];
913 [
self setClientId:123 configuration:config];
915 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
918 __block
int callCount = 0;
919 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
923 XCTAssertTrue(inputView.isSecureTextEntry);
925 config =
self.mutableTemplateCopy;
926 [config setValue:@"NO" forKey:@"obscureText"];
927 [
self updateConfig:config];
929 XCTAssertEqual(callCount, 1);
930 XCTAssertFalse(inputView.isSecureTextEntry);
933- (void)testInputActionContinueAction {
949 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
951 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
954- (void)testDisablingAutocorrectDisablesSpellChecking {
958 NSDictionary* config =
self.mutableTemplateCopy;
959 [inputView configureWithDictionary:config];
961 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
962 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
964 [config setValue:@(NO) forKey:@"autocorrect"];
965 [inputView configureWithDictionary:config];
967 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
968 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
971- (void)testEnableInlinePredictionFromConfiguration
API_AVAILABLE(ios(17.0)) {
973 NSMutableDictionary* config =
self.mutableTemplateCopy;
976 [inputView configureWithDictionary:config];
977 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
979 [config setValue:@NO forKey:@"enableInlinePrediction"];
980 [inputView configureWithDictionary:config];
981 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
983 [config setValue:@YES forKey:@"enableInlinePrediction"];
984 [inputView configureWithDictionary:config];
985 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeYes);
988 [config removeObjectForKey:@"enableInlinePrediction"];
989 [inputView configureWithDictionary:config];
990 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
993 [config setValue:[NSNull null] forKey:@"enableInlinePrediction"];
994 [inputView configureWithDictionary:config];
995 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
998- (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
1000 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1014 XCTAssertEqual(inputView.markedTextRange, nil);
1017- (void)testFlutterTextInputViewIsNotClearWhenKeyboardShowAndHide {
1020 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1021 XCTAssertEqualObjects(inputView.text,
@"test text");
1024 [
self setTextInputShow];
1025 XCTAssertEqualObjects(inputView.text,
@"test text");
1028 [
self setTextInputHide];
1029 XCTAssertEqualObjects(inputView.text,
@"test text");
1032- (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
1036 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
1037 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
1038 if (@available(iOS 17, *)) {
1039 XCTAssertFalse(respondsToInsertionPointColor);
1041 XCTAssertTrue(respondsToInsertionPointColor);
1045- (void)testSetAttributedMarkedTextSelectedRange
API_AVAILABLE(ios(17.0)) {
1047 NSAttributedString* attributedText =
1048 [[NSAttributedString alloc] initWithString:@"inline prediction"
1050 NSForegroundColorAttributeName : [UIColor grayColor],
1052 [inputView setAttributedMarkedText:attributedText selectedRange:NSMakeRange(0, 7)];
1054 XCTAssertEqualObjects(inputView.text,
@"inline prediction");
1055 NSRange selectedRange = ((
FlutterTextRange*)inputView.selectedTextRange).range;
1056 XCTAssertEqual(selectedRange.location, 0ul);
1057 XCTAssertEqual(selectedRange.length, 7ul);
1059 XCTAssertNotNil(markedRange);
1060 XCTAssertEqual(markedRange.range.location, 0ul);
1062 XCTAssertEqual(markedRange.range.length, 17ul);
1065 [inputView setAttributedMarkedText:nil selectedRange:NSMakeRange(0, 0)];
1066 XCTAssertEqualObjects(inputView.text,
@"");
1067 XCTAssertNil(inputView.markedTextRange);
1070#pragma mark - TextEditingDelta tests
1071- (void)testTextEditingDeltasAreGeneratedOnTextInput {
1073 inputView.enableDeltaModel = YES;
1075 __block
int updateCount = 0;
1077 [inputView insertText:@"text to insert"];
1080 flutterTextInputView:inputView
1081 updateEditingClient:0
1082 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1083 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1084 isEqualToString:
@""]) &&
1085 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1086 isEqualToString:
@"text to insert"]) &&
1087 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1088 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
1090 .andDo(^(NSInvocation* invocation) {
1093 XCTAssertEqual(updateCount, 0);
1095 [
self flushScheduledAsyncBlocks];
1098 XCTAssertEqual(updateCount, 1);
1100 [inputView deleteBackward];
1101 OCMExpect([
engine flutterTextInputView:inputView
1102 updateEditingClient:0
1103 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1104 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1105 isEqualToString:
@"text to insert"]) &&
1106 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1107 isEqualToString:
@""]) &&
1108 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1110 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1113 .andDo(^(NSInvocation* invocation) {
1116 [
self flushScheduledAsyncBlocks];
1117 XCTAssertEqual(updateCount, 2);
1120 OCMExpect([
engine flutterTextInputView:inputView
1121 updateEditingClient:0
1122 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1123 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1124 isEqualToString:
@"text to inser"]) &&
1125 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1126 isEqualToString:
@""]) &&
1127 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1129 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1132 .andDo(^(NSInvocation* invocation) {
1135 [
self flushScheduledAsyncBlocks];
1136 XCTAssertEqual(updateCount, 3);
1139 withText:@"replace text"];
1142 flutterTextInputView:inputView
1143 updateEditingClient:0
1144 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1145 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1146 isEqualToString:
@"text to inser"]) &&
1147 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1148 isEqualToString:
@"replace text"]) &&
1149 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1150 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
1152 .andDo(^(NSInvocation* invocation) {
1155 [
self flushScheduledAsyncBlocks];
1156 XCTAssertEqual(updateCount, 4);
1158 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1159 OCMExpect([
engine flutterTextInputView:inputView
1160 updateEditingClient:0
1161 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1162 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1163 isEqualToString:
@"replace textext to inser"]) &&
1164 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1165 isEqualToString:
@"marked text"]) &&
1166 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1168 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1171 .andDo(^(NSInvocation* invocation) {
1174 [
self flushScheduledAsyncBlocks];
1175 XCTAssertEqual(updateCount, 5);
1177 [inputView unmarkText];
1179 flutterTextInputView:inputView
1180 updateEditingClient:0
1181 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1182 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1183 isEqualToString:
@"replace textmarked textext to inser"]) &&
1184 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1185 isEqualToString:
@""]) &&
1186 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1188 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1191 .andDo(^(NSInvocation* invocation) {
1194 [
self flushScheduledAsyncBlocks];
1196 XCTAssertEqual(updateCount, 6);
1200- (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1203 inputView.enableDeltaModel = YES;
1206 OCMExpect([
engine flutterTextInputView:inputView
1207 updateEditingClient:0
1208 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1209 NSArray* deltas = state[@"deltas"];
1210 NSDictionary* firstDelta = deltas[0];
1211 NSDictionary* secondDelta = deltas[1];
1212 NSDictionary* thirdDelta = deltas[2];
1213 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1214 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1215 [firstDelta[@"deltaStart"] intValue] == 0 &&
1216 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1217 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1218 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1219 [secondDelta[@"deltaStart"] intValue] == 0 &&
1220 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1221 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1222 [thirdDelta[@"deltaText"] isEqualToString:@"—"] &&
1223 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1224 [thirdDelta[@"deltaEnd"] intValue] == 0;
1228 [inputView insertText:@"-"];
1229 [inputView deleteBackward];
1230 [inputView insertText:@"—"];
1232 [
self flushScheduledAsyncBlocks];
1236- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1238 inputView.enableDeltaModel = YES;
1240 __block
int updateCount = 0;
1241 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1242 .andDo(^(NSInvocation* invocation) {
1246 [inputView.text setString:@"Some initial text"];
1247 XCTAssertEqual(updateCount, 0);
1250 inputView.markedTextRange = range;
1251 inputView.selectedTextRange = nil;
1252 [
self flushScheduledAsyncBlocks];
1253 XCTAssertEqual(updateCount, 1);
1255 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1257 flutterTextInputView:inputView
1258 updateEditingClient:0
1259 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1260 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1261 isEqualToString:
@"Some initial text"]) &&
1262 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1263 isEqualToString:
@"new marked text."]) &&
1264 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1265 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1267 [
self flushScheduledAsyncBlocks];
1268 XCTAssertEqual(updateCount, 2);
1271- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1273 inputView.enableDeltaModel = YES;
1275 __block
int updateCount = 0;
1276 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1277 .andDo(^(NSInvocation* invocation) {
1281 [inputView.text setString:@"Some initial text"];
1282 [
self flushScheduledAsyncBlocks];
1283 XCTAssertEqual(updateCount, 0);
1286 inputView.markedTextRange = range;
1287 inputView.selectedTextRange = nil;
1288 [
self flushScheduledAsyncBlocks];
1289 XCTAssertEqual(updateCount, 1);
1291 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1293 flutterTextInputView:inputView
1294 updateEditingClient:0
1295 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1296 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1297 isEqualToString:
@"Some initial text"]) &&
1298 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1299 isEqualToString:
@"text."]) &&
1300 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1301 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1303 [
self flushScheduledAsyncBlocks];
1304 XCTAssertEqual(updateCount, 2);
1307- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1309 inputView.enableDeltaModel = YES;
1311 __block
int updateCount = 0;
1312 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1313 .andDo(^(NSInvocation* invocation) {
1317 [inputView.text setString:@"Some initial text"];
1318 [
self flushScheduledAsyncBlocks];
1319 XCTAssertEqual(updateCount, 0);
1322 inputView.markedTextRange = range;
1323 inputView.selectedTextRange = nil;
1324 [
self flushScheduledAsyncBlocks];
1325 XCTAssertEqual(updateCount, 1);
1327 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1329 flutterTextInputView:inputView
1330 updateEditingClient:0
1331 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1332 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1333 isEqualToString:
@"Some initial text"]) &&
1334 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1335 isEqualToString:
@"tex"]) &&
1336 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1337 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1339 [
self flushScheduledAsyncBlocks];
1340 XCTAssertEqual(updateCount, 2);
1343#pragma mark - EditingState tests
1345- (void)testUITextInputCallsUpdateEditingStateOnce {
1348 __block
int updateCount = 0;
1349 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1350 .andDo(^(NSInvocation* invocation) {
1354 [inputView insertText:@"text to insert"];
1356 XCTAssertEqual(updateCount, 1);
1358 [inputView deleteBackward];
1359 XCTAssertEqual(updateCount, 2);
1362 XCTAssertEqual(updateCount, 3);
1365 withText:@"replace text"];
1366 XCTAssertEqual(updateCount, 4);
1368 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1369 XCTAssertEqual(updateCount, 5);
1371 [inputView unmarkText];
1372 XCTAssertEqual(updateCount, 6);
1375- (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1377 inputView.enableDeltaModel = YES;
1379 __block
int updateCount = 0;
1380 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1381 .andDo(^(NSInvocation* invocation) {
1385 [inputView insertText:@"text to insert"];
1386 [
self flushScheduledAsyncBlocks];
1388 XCTAssertEqual(updateCount, 1);
1390 [inputView deleteBackward];
1391 [
self flushScheduledAsyncBlocks];
1392 XCTAssertEqual(updateCount, 2);
1395 [
self flushScheduledAsyncBlocks];
1396 XCTAssertEqual(updateCount, 3);
1399 withText:@"replace text"];
1400 [
self flushScheduledAsyncBlocks];
1401 XCTAssertEqual(updateCount, 4);
1403 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1404 [
self flushScheduledAsyncBlocks];
1405 XCTAssertEqual(updateCount, 5);
1407 [inputView unmarkText];
1408 [
self flushScheduledAsyncBlocks];
1409 XCTAssertEqual(updateCount, 6);
1412- (void)testTextChangesDoNotTriggerUpdateEditingClient {
1415 __block
int updateCount = 0;
1416 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1417 .andDo(^(NSInvocation* invocation) {
1421 [inputView.text setString:@"BEFORE"];
1422 XCTAssertEqual(updateCount, 0);
1424 inputView.markedTextRange = nil;
1425 inputView.selectedTextRange = nil;
1426 XCTAssertEqual(updateCount, 1);
1429 XCTAssertEqual(updateCount, 1);
1430 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1431 XCTAssertEqual(updateCount, 1);
1432 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1433 XCTAssertEqual(updateCount, 1);
1437 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1438 XCTAssertEqual(updateCount, 1);
1440 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1441 XCTAssertEqual(updateCount, 1);
1445 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1446 XCTAssertEqual(updateCount, 1);
1448 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1449 XCTAssertEqual(updateCount, 1);
1452- (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1454 inputView.enableDeltaModel = YES;
1456 __block
int updateCount = 0;
1457 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1458 .andDo(^(NSInvocation* invocation) {
1462 [inputView.text setString:@"BEFORE"];
1463 [
self flushScheduledAsyncBlocks];
1464 XCTAssertEqual(updateCount, 0);
1466 inputView.markedTextRange = nil;
1467 inputView.selectedTextRange = nil;
1468 [
self flushScheduledAsyncBlocks];
1469 XCTAssertEqual(updateCount, 1);
1472 XCTAssertEqual(updateCount, 1);
1473 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1474 [
self flushScheduledAsyncBlocks];
1475 XCTAssertEqual(updateCount, 1);
1477 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1478 [
self flushScheduledAsyncBlocks];
1479 XCTAssertEqual(updateCount, 1);
1483 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1484 [
self flushScheduledAsyncBlocks];
1485 XCTAssertEqual(updateCount, 1);
1488 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1489 [
self flushScheduledAsyncBlocks];
1490 XCTAssertEqual(updateCount, 1);
1494 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1495 [
self flushScheduledAsyncBlocks];
1496 XCTAssertEqual(updateCount, 1);
1499 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1500 [
self flushScheduledAsyncBlocks];
1501 XCTAssertEqual(updateCount, 1);
1504- (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1507 __block
int updateCount = 0;
1508 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1509 .andDo(^(NSInvocation* invocation) {
1513 [inputView unmarkText];
1515 XCTAssertEqual(updateCount, 0);
1517 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1519 XCTAssertEqual(updateCount, 1);
1521 [inputView unmarkText];
1523 XCTAssertEqual(updateCount, 2);
1526- (void)testCanCopyPasteWithScribbleEnabled {
1527 if (@available(iOS 14.0, *)) {
1528 NSDictionary* config =
self.mutableTemplateCopy;
1529 [
self setClientId:123 configuration:config];
1530 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1536 [mockInputView insertText:@"aaaa"];
1537 [mockInputView selectAll:nil];
1539 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1540 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1541 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1542 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1544 [mockInputView copy:NULL];
1545 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1546 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1547 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1548 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1552- (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1553 if (@available(iOS 14.0, *)) {
1556 __block
int updateCount = 0;
1557 OCMStub([
engine flutterTextInputView:inputView
1558 updateEditingClient:0
1559 withState:[OCMArg isNotNil]])
1560 .andDo(^(NSInvocation* invocation) {
1564 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1566 XCTAssertEqual(updateCount, 1);
1568 UIScribbleInteraction* scribbleInteraction =
1569 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1571 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1572 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1574 XCTAssertEqual(updateCount, 1);
1576 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1577 [inputView resetScribbleInteractionStatusIfEnding];
1578 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1580 XCTAssertEqual(updateCount, 2);
1582 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1583 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1586 XCTAssertEqual(updateCount, 2);
1588 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1589 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1592 XCTAssertEqual(updateCount, 2);
1594 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1595 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1597 XCTAssertEqual(updateCount, 3);
1601- (void)testUpdateEditingClientNegativeSelection {
1604 [inputView.text setString:@"SELECTION"];
1605 inputView.markedTextRange = nil;
1606 inputView.selectedTextRange = nil;
1608 [inputView setTextInputState:@{
1609 @"text" : @"SELECTION",
1610 @"selectionBase" : @-1,
1611 @"selectionExtent" : @-1
1613 [inputView updateEditingState];
1614 OCMVerify([
engine flutterTextInputView:inputView
1615 updateEditingClient:0
1616 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1617 return ([state[
@"selectionBase"] intValue]) == 0 &&
1618 ([state[@"selectionExtent"] intValue] == 0);
1623 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1624 [inputView updateEditingState];
1625 OCMVerify([
engine flutterTextInputView:inputView
1626 updateEditingClient:0
1627 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1628 return ([state[
@"selectionBase"] intValue]) == 0 &&
1629 ([state[@"selectionExtent"] intValue] == 0);
1633 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1634 [inputView updateEditingState];
1635 OCMVerify([
engine flutterTextInputView:inputView
1636 updateEditingClient:0
1637 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1638 return ([state[
@"selectionBase"] intValue]) == 0 &&
1639 ([state[@"selectionExtent"] intValue] == 0);
1643- (void)testUpdateEditingClientSelectionClamping {
1647 [inputView.text setString:@"SELECTION"];
1648 inputView.markedTextRange = nil;
1649 inputView.selectedTextRange = nil;
1652 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1653 [inputView updateEditingState];
1654 OCMVerify([
engine flutterTextInputView:inputView
1655 updateEditingClient:0
1656 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1657 return ([state[
@"selectionBase"] intValue]) == 0 &&
1658 ([state[@"selectionExtent"] intValue] == 0);
1662 [inputView setTextInputState:@{
1663 @"text" : @"SELECTION",
1664 @"selectionBase" : @0,
1665 @"selectionExtent" : @9999
1667 [inputView updateEditingState];
1669 OCMVerify([
engine flutterTextInputView:inputView
1670 updateEditingClient:0
1671 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1672 return ([state[
@"selectionBase"] intValue]) == 0 &&
1673 ([state[@"selectionExtent"] intValue] == 9);
1678 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1679 [inputView updateEditingState];
1680 OCMVerify([
engine flutterTextInputView:inputView
1681 updateEditingClient:0
1682 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1683 return ([state[
@"selectionBase"] intValue]) == 0 &&
1684 ([state[@"selectionExtent"] intValue] == 1);
1688 [inputView setTextInputState:@{
1689 @"text" : @"SELECTION",
1690 @"selectionBase" : @9999,
1691 @"selectionExtent" : @9999
1693 [inputView updateEditingState];
1694 OCMVerify([
engine flutterTextInputView:inputView
1695 updateEditingClient:0
1696 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1697 return ([state[
@"selectionBase"] intValue]) == 9 &&
1698 ([state[@"selectionExtent"] intValue] == 9);
1702- (void)testInputViewsHasNonNilInputDelegate {
1704 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1706 [inputView setTextInputClient:123];
1707 [inputView reloadInputViews];
1708 [inputView becomeFirstResponder];
1709 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1710 inputView.inputDelegate = nil;
1714 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1715 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1716 [inputView removeFromSuperview];
1719- (void)testInputViewsDoNotHaveUITextInteractions {
1721 BOOL hasTextInteraction = NO;
1722 for (
id interaction in inputView.interactions) {
1723 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1724 if (hasTextInteraction) {
1728 XCTAssertFalse(hasTextInteraction);
1731#pragma mark - UITextInput methods - Tests
1733- (void)testUpdateFirstRectForRange {
1734 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1740 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1745 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1746 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1750 NSArray* affineMatrix = @[
1751 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1752 @(-6.0), @(3.0), @(9.0), @(1.0)
1756 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1758 [inputView setEditableTransform:yOffsetMatrix];
1760 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1763 CGRect testRect = CGRectMake(0, 0, 100, 100);
1764 [inputView setMarkedRect:testRect];
1766 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1767 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1769 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1772 [inputView setEditableTransform:zeroMatrix];
1774 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1775 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1778 [inputView setEditableTransform:yOffsetMatrix];
1779 [inputView setMarkedRect:testRect];
1780 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1783 [inputView setMarkedRect:kInvalidFirstRect];
1785 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1786 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1789 [inputView setEditableTransform:affineMatrix];
1790 [inputView setMarkedRect:testRect];
1792 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1794 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1795 const CGPoint offset = CGPointMake(113, 119);
1796 CGRect currentFrame = inputView.frame;
1797 currentFrame.origin = offset;
1798 inputView.frame = currentFrame;
1801 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1802 [inputView firstRectForRange:range]));
1805- (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1807 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1812 [inputView setSelectionRects:@[
1821 if (@available(iOS 17, *)) {
1822 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1823 [inputView firstRectForRange:multiRectRange]));
1825 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1826 [inputView firstRectForRange:multiRectRange]));
1830- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1832 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1834 [inputView setSelectionRects:@[
1841 if (@available(iOS 17, *)) {
1842 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1843 [inputView firstRectForRange:singleRectRange]));
1845 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1850 if (@available(iOS 17, *)) {
1851 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1852 [inputView firstRectForRange:multiRectRange]));
1854 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1857 [inputView setTextInputState:@{@"text" : @"COM"}];
1859 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1862- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1864 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1866 [inputView setSelectionRects:@[
1873 if (@available(iOS 17, *)) {
1874 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1875 [inputView firstRectForRange:singleRectRange]));
1877 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1881 if (@available(iOS 17, *)) {
1882 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1883 [inputView firstRectForRange:multiRectRange]));
1885 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1888 [inputView setTextInputState:@{@"text" : @"COM"}];
1890 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1893- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1895 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1897 [inputView setSelectionRects:@[
1908 if (@available(iOS 17, *)) {
1909 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1910 [inputView firstRectForRange:singleRectRange]));
1912 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1917 if (@available(iOS 17, *)) {
1918 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1919 [inputView firstRectForRange:multiRectRange]));
1921 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1925- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1927 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1929 [inputView setSelectionRects:@[
1940 if (@available(iOS 17, *)) {
1941 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1942 [inputView firstRectForRange:singleRectRange]));
1944 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1948 if (@available(iOS 17, *)) {
1949 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1950 [inputView firstRectForRange:multiRectRange]));
1952 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1956- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1958 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1960 [inputView setSelectionRects:@[
1971 if (@available(iOS 17, *)) {
1972 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1973 [inputView firstRectForRange:multiRectRange]));
1975 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1979- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1981 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1983 [inputView setSelectionRects:@[
1994 if (@available(iOS 17, *)) {
1995 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1996 [inputView firstRectForRange:multiRectRange]));
1998 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2002- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
2004 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2006 [inputView setSelectionRects:@[
2017 if (@available(iOS 17, *)) {
2018 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
2019 [inputView firstRectForRange:multiRectRange]));
2021 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2025- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
2027 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2029 [inputView setSelectionRects:@[
2040 if (@available(iOS 17, *)) {
2041 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
2042 [inputView firstRectForRange:multiRectRange]));
2044 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2048- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
2050 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2052 [inputView setSelectionRects:@[
2063 if (@available(iOS 17, *)) {
2064 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
2065 [inputView firstRectForRange:multiRectRange]));
2067 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2071- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
2073 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2075 [inputView setSelectionRects:@[
2086 if (@available(iOS 17, *)) {
2087 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
2088 [inputView firstRectForRange:multiRectRange]));
2090 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2094- (void)testClosestPositionToPoint {
2096 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2099 [inputView setSelectionRects:@[
2104 CGPoint point = CGPointMake(150, 150);
2105 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2106 XCTAssertEqual(UITextStorageDirectionBackward,
2111 [inputView setSelectionRects:@[
2118 point = CGPointMake(125, 150);
2119 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2120 XCTAssertEqual(UITextStorageDirectionForward,
2125 [inputView setSelectionRects:@[
2132 point = CGPointMake(125, 201);
2133 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2134 XCTAssertEqual(UITextStorageDirectionBackward,
2138 [inputView setSelectionRects:@[
2144 point = CGPointMake(125, 250);
2145 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2146 XCTAssertEqual(UITextStorageDirectionBackward,
2150 [inputView setSelectionRects:@[
2155 point = CGPointMake(110, 50);
2156 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2157 XCTAssertEqual(UITextStorageDirectionForward,
2162 [inputView beginFloatingCursorAtPoint:CGPointZero];
2163 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2164 XCTAssertEqual(UITextStorageDirectionForward,
2166 [inputView endFloatingCursor];
2169- (void)testClosestPositionToPointRTL {
2171 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2173 [inputView setSelectionRects:@[
2189 XCTAssertEqual(0U, position.
index);
2190 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2192 XCTAssertEqual(1U, position.
index);
2193 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2195 XCTAssertEqual(1U, position.
index);
2196 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2198 XCTAssertEqual(2U, position.
index);
2199 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2201 XCTAssertEqual(2U, position.
index);
2202 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2204 XCTAssertEqual(3U, position.
index);
2205 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2207 XCTAssertEqual(3U, position.
index);
2208 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2211- (void)testSelectionRectsForRange {
2213 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2215 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2216 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2217 [inputView setSelectionRects:@[
2226 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2227 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2228 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2232 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2233 XCTAssertTrue(CGRectEqualToRect(
2234 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2235 [inputView selectionRectsForRange:range][0].rect));
2238- (void)testClosestPositionToPointWithinRange {
2240 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2243 [inputView setSelectionRects:@[
2250 CGPoint point = CGPointMake(125, 150);
2253 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2255 UITextStorageDirectionForward,
2256 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2259 [inputView setSelectionRects:@[
2266 point = CGPointMake(125, 150);
2269 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2271 UITextStorageDirectionForward,
2272 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2275- (void)testClosestPositionToPointWithPartialSelectionRects {
2277 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2284 XCTAssertTrue(CGRectEqualToRect(
2287 affinity:UITextStorageDirectionForward]],
2288 CGRectMake(100, 0, 0, 100)));
2291 XCTAssertTrue(CGRectEqualToRect(
2294 affinity:UITextStorageDirectionForward]],
2298#pragma mark - Floating Cursor - Tests
2300- (void)testFloatingCursorDoesNotThrow {
2303 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2304 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2305 [inputView endFloatingCursor];
2306 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2307 [inputView endFloatingCursor];
2310- (void)testFloatingCursor {
2312 [inputView setTextInputState:@{
2314 @"selectionBase" : @1,
2315 @"selectionExtent" : @1,
2326 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2329 XCTAssertTrue(CGRectEqualToRect(
2332 affinity:UITextStorageDirectionForward]],
2333 CGRectMake(0, 0, 0, 100)));
2336 XCTAssertTrue(CGRectEqualToRect(
2339 affinity:UITextStorageDirectionForward]],
2340 CGRectMake(100, 100, 0, 100)));
2341 XCTAssertTrue(CGRectEqualToRect(
2344 affinity:UITextStorageDirectionForward]],
2345 CGRectMake(200, 200, 0, 100)));
2346 XCTAssertTrue(CGRectEqualToRect(
2349 affinity:UITextStorageDirectionForward]],
2350 CGRectMake(300, 300, 0, 100)));
2353 XCTAssertTrue(CGRectEqualToRect(
2356 affinity:UITextStorageDirectionForward]],
2357 CGRectMake(400, 300, 0, 100)));
2359 XCTAssertTrue(CGRectEqualToRect(
2362 affinity:UITextStorageDirectionForward]],
2366 [inputView setTextInputState:@{
2368 @"selectionBase" : @2,
2369 @"selectionExtent" : @2,
2372 XCTAssertTrue(CGRectEqualToRect(
2375 affinity:UITextStorageDirectionBackward]],
2376 CGRectMake(0, 0, 0, 100)));
2379 XCTAssertTrue(CGRectEqualToRect(
2382 affinity:UITextStorageDirectionBackward]],
2383 CGRectMake(100, 0, 0, 100)));
2384 XCTAssertTrue(CGRectEqualToRect(
2387 affinity:UITextStorageDirectionBackward]],
2388 CGRectMake(200, 100, 0, 100)));
2389 XCTAssertTrue(CGRectEqualToRect(
2392 affinity:UITextStorageDirectionBackward]],
2393 CGRectMake(300, 200, 0, 100)));
2394 XCTAssertTrue(CGRectEqualToRect(
2397 affinity:UITextStorageDirectionBackward]],
2398 CGRectMake(400, 300, 0, 100)));
2400 XCTAssertTrue(CGRectEqualToRect(
2403 affinity:UITextStorageDirectionBackward]],
2408 CGRect initialBounds = inputView.bounds;
2409 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2410 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2411 OCMVerify([
engine flutterTextInputView:inputView
2412 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2414 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2415 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2416 ([state[
@"Y"] isEqualToNumber:@(0)]);
2419 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2420 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2421 OCMVerify([
engine flutterTextInputView:inputView
2422 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2424 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2425 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2426 ([state[
@"Y"] isEqualToNumber:@(333)]);
2429 [inputView endFloatingCursor];
2430 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2431 OCMVerify([
engine flutterTextInputView:inputView
2432 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2434 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2435 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2436 ([state[
@"Y"] isEqualToNumber:@(0)]);
2440#pragma mark - UIKeyInput Overrides - Tests
2442- (void)testInsertTextAddsPlaceholderSelectionRects {
2445 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2455 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2458 [inputView insertText:@"in"];
2486#pragma mark - Autofill - Utilities
2488- (NSMutableDictionary*)mutablePasswordTemplateCopy {
2491 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2492 @"keyboardAppearance" :
@"Brightness.light",
2493 @"obscureText" : @YES,
2494 @"inputAction" :
@"TextInputAction.unspecified",
2495 @"smartDashesType" :
@"0",
2496 @"smartQuotesType" :
@"0",
2497 @"autocorrect" : @YES
2501 return [_passwordTemplate mutableCopy];
2505 return [
self.installedInputViews
2506 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2509- (void)commitAutofillContextAndVerify {
2513 [textInputPlugin handleMethodCall:methodCall
2514 result:^(id _Nullable result){
2517 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2522 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2526#pragma mark - Autofill - Tests
2528- (void)testDisablingAutofillOnInputClient {
2529 NSDictionary* config =
self.mutableTemplateCopy;
2530 [config setValue:@"YES" forKey:@"obscureText"];
2532 [
self setClientId:123 configuration:config];
2535 XCTAssertEqualObjects(inputView.textContentType,
@"");
2538- (void)testAutofillEnabledByDefault {
2539 NSDictionary* config =
self.mutableTemplateCopy;
2540 [config setValue:@"NO" forKey:@"obscureText"];
2541 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2542 forKey:@"autofill"];
2544 [
self setClientId:123 configuration:config];
2547 XCTAssertNil(inputView.textContentType);
2550- (void)testAutofillContext {
2551 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2554 @"uniqueIdentifier" : @"field1",
2555 @"hints" : @[ @"hint1" ],
2556 @"editingValue" : @{@"text" : @""}
2558 forKey:@"autofill"];
2560 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2562 @"uniqueIdentifier" : @"field2",
2563 @"hints" : @[ @"hint2" ],
2564 @"editingValue" : @{@"text" : @""}
2566 forKey:@"autofill"];
2568 NSMutableDictionary* config = [field1 mutableCopy];
2569 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2571 [
self setClientId:123 configuration:config];
2572 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2576 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2577 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2579 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2582 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2584 @"uniqueIdentifier" : @"field3",
2585 @"hints" : @[ @"hint3" ],
2586 @"editingValue" : @{@"text" : @""}
2588 forKey:@"autofill"];
2592 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2594 [
self setClientId:123 configuration:config];
2596 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2599 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2600 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2602 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2605 for (NSString*
key in oldContext.allKeys) {
2610 config =
self.mutablePasswordTemplateCopy;
2613 [
self setClientId:124 configuration:config];
2614 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2616 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2619 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2620 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2623 for (NSString*
key in oldContext.allKeys) {
2628 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2632 [
self setClientId:200 configuration:config];
2635 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2638 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2639 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2642 for (NSString*
key in oldContext.allKeys) {
2646 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2649- (void)testCommitAutofillContext {
2650 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2652 @"uniqueIdentifier" : @"field1",
2653 @"hints" : @[ @"hint1" ],
2654 @"editingValue" : @{@"text" : @""}
2656 forKey:@"autofill"];
2658 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2660 @"uniqueIdentifier" : @"field2",
2661 @"hints" : @[ @"hint2" ],
2662 @"editingValue" : @{@"text" : @""}
2664 forKey:@"autofill"];
2666 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2668 @"uniqueIdentifier" : @"field3",
2669 @"hints" : @[ @"hint3" ],
2670 @"editingValue" : @{@"text" : @""}
2672 forKey:@"autofill"];
2674 NSMutableDictionary* config = [field1 mutableCopy];
2675 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2677 [
self setClientId:123 configuration:config];
2678 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2680 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2682 [
self commitAutofillContextAndVerify];
2683 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2686 [
self setClientId:123 configuration:config];
2688 [
self setClientId:124 configuration:field3];
2689 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2691 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2692 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2695 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2697 [
self commitAutofillContextAndVerify];
2698 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2701 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2703 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2707 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2708 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2710 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2712 [
self commitAutofillContextAndVerify];
2713 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2716- (void)testAutofillInputViews {
2717 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2719 @"uniqueIdentifier" : @"field1",
2720 @"hints" : @[ @"hint1" ],
2721 @"editingValue" : @{@"text" : @""}
2723 forKey:@"autofill"];
2725 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2727 @"uniqueIdentifier" : @"field2",
2728 @"hints" : @[ @"hint2" ],
2729 @"editingValue" : @{@"text" : @""}
2731 forKey:@"autofill"];
2733 NSMutableDictionary* config = [field1 mutableCopy];
2734 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2736 [
self setClientId:123 configuration:config];
2737 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2740 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2743 XCTAssertEqual(inputFields.count, 2ul);
2744 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2749 withText:@"Autofilled!"];
2750 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2753 OCMVerify([
engine flutterTextInputView:inactiveView
2754 updateEditingClient:0
2755 withState:[OCMArg isNotNil]
2756 withTag:
@"field2"]);
2759- (void)testAutofillContextPersistsAfterClearClient {
2760 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2762 @"uniqueIdentifier" : @"field1",
2763 @"hints" : @[ @"username" ],
2764 @"editingValue" : @{@"text" : @""}
2766 forKey:@"autofill"];
2768 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2770 @"uniqueIdentifier" : @"field2",
2771 @"hints" : @[ @"password" ],
2772 @"editingValue" : @{@"text" : @""}
2774 forKey:@"autofill"];
2776 NSMutableDictionary* config = [field1 mutableCopy];
2777 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2780 [
self setClientId:123 configuration:config];
2785 [
self setClientClear];
2790 [
self commitAutofillContextAndVerify];
2795- (void)testSetClientResetsPendingAutofillRemoval {
2799 NSMutableDictionary* field =
self.mutablePasswordTemplateCopy;
2801 @"uniqueIdentifier" : @"field1",
2802 @"hints" : @[ @"password" ],
2803 @"editingValue" : @{@"text" : @""}
2805 forKey:@"autofill"];
2806 [field setValue:@[ field ] forKey:@"fields"];
2809 [
self setClientId:123 configuration:field];
2813 [
self setClientClear];
2817 [
self setClientId:456 configuration:self.mutableTemplateCopy];
2822- (void)testPendingInputViewRemovalAfterClearClient {
2826 NSDictionary* config =
self.mutableTemplateCopy;
2829 [
self setClientId:123 configuration:config];
2836 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2839 [
self setClientClear];
2844 [
self setTextInputHide];
2849- (void)testHideBeforeClearClientRemovesViewImmediately {
2853 NSDictionary* config =
self.mutableTemplateCopy;
2855 [
self setClientId:123 configuration:config];
2856 [
self setTextInputShow];
2860 [
self setTextInputHide];
2865 [
self setClientClear];
2870- (void)testSetClientResetsPendingInputViewRemoval {
2874 NSDictionary* config =
self.mutableTemplateCopy;
2877 [
self setClientId:123 configuration:config];
2878 [
self setTextInputShow];
2883 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2885 [
self setClientClear];
2889 [
self setClientId:456 configuration:config];
2893- (void)testPasswordAutofillHack {
2894 NSDictionary* config =
self.mutableTemplateCopy;
2895 [config setValue:@"YES" forKey:@"obscureText"];
2896 [
self setClientId:123 configuration:config];
2899 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2903 XCTAssert([inputView isKindOfClass:[UITextField class]]);
2906 XCTAssertNotEqual([inputView performSelector:@selector(font)], nil);
2909- (void)testClearAutofillContextClearsSelection {
2910 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2911 NSDictionary* editingValue = @{
2912 @"text" :
@"REGULAR_TEXT_FIELD",
2913 @"composingBase" : @0,
2914 @"composingExtent" : @3,
2915 @"selectionBase" : @1,
2916 @"selectionExtent" : @4
2918 [regularField setValue:@{
2919 @"uniqueIdentifier" : @"field2",
2920 @"hints" : @[ @"hint2" ],
2921 @"editingValue" : editingValue,
2923 forKey:@"autofill"];
2924 [regularField addEntriesFromDictionary:editingValue];
2925 [
self setClientId:123 configuration:regularField];
2926 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2927 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2930 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2932 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(1, 3)));
2936 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2937 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2939 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2941 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2942 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2945 XCTAssert([oldInputView.text isEqualToString:
@""]);
2947 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(0, 0)));
2950- (void)testGarbageInputViewsAreNotRemovedImmediately {
2952 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2953 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2955 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2958 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2959 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2961 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2963 [
self commitAutofillContextAndVerify];
2966- (void)testScribbleSetSelectionRects {
2967 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2968 NSDictionary* editingValue = @{
2969 @"text" :
@"REGULAR_TEXT_FIELD",
2970 @"composingBase" : @0,
2971 @"composingExtent" : @3,
2972 @"selectionBase" : @1,
2973 @"selectionExtent" : @4
2975 [regularField setValue:@{
2976 @"uniqueIdentifier" : @"field1",
2977 @"hints" : @[ @"hint2" ],
2978 @"editingValue" : editingValue,
2980 forKey:@"autofill"];
2981 [regularField addEntriesFromDictionary:editingValue];
2982 [
self setClientId:123 configuration:regularField];
2983 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2984 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2986 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2987 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2991 [textInputPlugin handleMethodCall:methodCall
2992 result:^(id _Nullable result){
2995 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
2998- (void)testDecommissionedViewAreNotReusedByAutofill {
3000 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
3001 [configuration setValue:@{
3002 @"uniqueIdentifier" : @"field1",
3003 @"hints" : @[ UITextContentTypePassword ],
3004 @"editingValue" : @{@"text" : @""}
3006 forKey:@"autofill"];
3007 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3009 [
self setClientId:123 configuration:configuration];
3011 [
self setTextInputHide];
3014 [
self setClientId:124 configuration:configuration];
3018 XCTAssertNotNil(previousActiveView);
3022- (void)testInitialActiveViewCantAccessTextInputDelegate {
3029- (void)testAutoFillDoesNotTriggerOnShowAndHideKeyboard {
3031 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
3032 [configuration setValue:@{
3033 @"uniqueIdentifier" : @"field1",
3034 @"hints" : @[ UITextContentTypePassword ],
3035 @"editingValue" : @{@"text" : @""}
3037 forKey:@"autofill"];
3038 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3039 [
self setClientId:123 configuration:configuration];
3041 [
self setTextInputShow];
3042 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3045 [
self setTextInputHide];
3046 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3048 [
self commitAutofillContextAndVerify];
3051#pragma mark - Accessibility - Tests
3053- (void)testUITextInputAccessibilityNotHiddenWhenKeyboardIsShownAndHidden {
3054 [
self setClientId:123 configuration:self.mutableTemplateCopy];
3057 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
3060 XCTAssertEqual([inputFields count], 1u);
3063 [
self setTextInputShow];
3065 inputFields =
self.installedInputViews;
3067 XCTAssertEqual([inputFields count], 1u);
3070 [
self setTextInputHide];
3072 inputFields =
self.installedInputViews;
3074 XCTAssertEqual([inputFields count], 1u);
3077 [
self setClientClear];
3079 inputFields =
self.installedInputViews;
3082 XCTAssertEqual([inputFields count], 0u);
3085- (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
3088 UIView* container = [[UIView alloc] init];
3089 UIAccessibilityElement* backing =
3090 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
3091 inputView.backingTextInputAccessibilityObject = backing;
3094 [inputView accessibilityElementDidBecomeFocused];
3100- (void)testFlutterTokenizerCanParseLines {
3102 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3105 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3106 XCTAssertEqual(range.
range.location, 0u);
3107 XCTAssertEqual(range.
range.length, 0u);
3109 [inputView insertText:@"how are you\nI am fine, Thank you"];
3111 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3112 XCTAssertEqual(range.
range.location, 0u);
3113 XCTAssertEqual(range.
range.length, 11u);
3115 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
3116 XCTAssertEqual(range.
range.location, 0u);
3117 XCTAssertEqual(range.
range.length, 11u);
3119 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
3120 XCTAssertEqual(range.
range.location, 0u);
3121 XCTAssertEqual(range.
range.length, 11u);
3123 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
3124 XCTAssertEqual(range.
range.location, 12u);
3125 XCTAssertEqual(range.
range.length, 20u);
3127 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
3128 XCTAssertEqual(range.
range.location, 12u);
3129 XCTAssertEqual(range.
range.length, 20u);
3131 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
3132 XCTAssertEqual(range.
range.location, 12u);
3133 XCTAssertEqual(range.
range.length, 20u);
3136- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
3138 [inputView insertText:@"0123456789\n012345"];
3139 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3142 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3143 withGranularity:UITextGranularityLine
3144 inDirection:UITextStorageDirectionBackward];
3145 XCTAssertEqual(range.
range.location, 11u);
3146 XCTAssertEqual(range.
range.length, 6u);
3149- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
3151 [inputView insertText:@"0123456789\n012345"];
3152 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3155 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3156 withGranularity:UITextGranularityLine
3157 inDirection:UITextStorageDirectionForward];
3158 if (@available(iOS 17.0, *)) {
3159 XCTAssertNil(range);
3161 XCTAssertEqual(range.
range.location, 11u);
3162 XCTAssertEqual(range.
range.length, 6u);
3166- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
3168 [inputView insertText:@"0123456789\n012345"];
3169 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3174 withGranularity:UITextGranularityLine
3175 inDirection:UITextStorageDirectionForward];
3176 if (@available(iOS 17.0, *)) {
3177 XCTAssertNil(range);
3179 XCTAssertEqual(range.
range.location, 0u);
3180 XCTAssertEqual(range.
range.length, 0u);
3184- (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
3189 __weak UIView* activeView;
3194 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
3197 result:^(id _Nullable result){
3203 result:^(id _Nullable result){
3205 XCTAssertNotNil(activeView);
3208 XCTAssertNotNil(activeView);
3211- (void)testFlutterTextInputPluginHostViewNilCrash {
3214 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
3217- (void)testFlutterTextInputPluginHostViewNotNil {
3223 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
3226- (void)testSetPlatformViewClient {
3233 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
3235 result:^(id _Nullable result){
3238 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
3241 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
3243 result:^(id _Nullable result){
3245 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
3248- (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
3249 if (@available(iOS 16.0, *)) {
3251 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3252 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
3253 @"editMenuInteraction setup delegate correctly");
3257- (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
3258 if (@available(iOS 16.0, *)) {
3262 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
3266- (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
3267 if (@available(iOS 16.0, *)) {
3272 [myViewController loadView];
3275 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3277 result:^(id _Nullable result){
3283 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3285 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3286 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3288 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3289 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3290 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3291 .andDo(^(NSInvocation* invocation) {
3293 [invocation retainArguments];
3294 UIEditMenuConfiguration* config;
3295 [invocation getArgument:&config atIndex:2];
3296 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
3297 @"UIEditMenuConfiguration must use automatic arrow direction.");
3298 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
3299 @"UIEditMenuConfiguration must have the correct point.");
3300 [expectation fulfill];
3303 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3304 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
3306 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3307 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3308 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3312- (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
3313 if (@available(iOS 16.0, *)) {
3318 [myViewController loadView];
3322 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3324 result:^(id _Nullable result){
3330 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3332 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3333 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3335 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3336 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3337 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3338 .andDo(^(NSInvocation* invocation) {
3339 [expectation fulfill];
3342 myInputView.frame = CGRectMake(10, 20, 30, 40);
3343 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3344 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3346 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3347 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3348 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3351 [myInputView editMenuInteraction:mockInteraction
3352 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3354 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3355 @"targetRectForConfiguration must return the correct target rect.");
3359- (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3360 if (@available(iOS 16.0, *)) {
3365 [myViewController loadView];
3369 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3371 result:^(id _Nullable result){
3377 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3379 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3380 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3382 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3383 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3384 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3385 .andDo(^(NSInvocation* invocation) {
3386 [expectation fulfill];
3389 myInputView.frame = CGRectMake(10, 20, 30, 40);
3390 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3391 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3393 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3394 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3395 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3397 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3399 action:@selector(copy:)
3401 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3403 action:@selector(paste:)
3405 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3407 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3408 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3409 suggestedActions:suggestedActions];
3410 XCTAssertEqualObjects(menu.children, suggestedActions,
3411 @"Must show suggested items by default.");
3415- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3416 if (@available(iOS 16.0, *)) {
3421 [myViewController loadView];
3425 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3427 result:^(id _Nullable result){
3433 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3435 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3436 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3438 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3439 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3440 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3441 .andDo(^(NSInvocation* invocation) {
3442 [expectation fulfill];
3445 myInputView.frame = CGRectMake(10, 20, 30, 40);
3446 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3447 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3449 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3450 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3452 BOOL shownEditMenu =
3453 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3454 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3455 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3457 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3459 action:@selector(copy:)
3461 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3463 action:@selector(paste:)
3465 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3467 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3468 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3469 suggestedActions:suggestedActions];
3471 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3472 XCTAssertEqualObjects(menu.children, expectedChildren);
3476- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3477 if (@available(iOS 16.0, *)) {
3482 [myViewController loadView];
3486 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3488 result:^(id _Nullable result){
3494 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3496 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3497 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3499 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3500 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3501 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3502 .andDo(^(NSInvocation* invocation) {
3503 [expectation fulfill];
3506 myInputView.frame = CGRectMake(10, 20, 30, 40);
3507 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3508 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3510 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3511 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3513 BOOL shownEditMenu =
3514 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3515 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3516 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3518 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3520 action:@selector(copy:)
3522 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3524 action:@selector(cut:)
3526 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3528 action:@selector(paste:)
3541 NSArray<UIMenuElement*>* suggestedActions = @[
3542 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3543 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3546 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3547 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3548 suggestedActions:suggestedActions];
3550 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3551 XCTAssertEqualObjects(menu.children, expectedActions);
3555- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3556 if (@available(iOS 16.0, *)) {
3561 [myViewController loadView];
3565 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3567 result:^(id _Nullable result){
3573 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3575 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3576 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3578 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3579 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3580 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3581 .andDo(^(NSInvocation* invocation) {
3582 [expectation fulfill];
3585 myInputView.frame = CGRectMake(10, 20, 30, 40);
3586 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3587 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3589 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3590 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3591 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3594 BOOL shownEditMenu =
3595 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3596 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3597 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3599 NSArray<UICommand*>* suggestedActions = @[
3600 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3603 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3604 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3605 suggestedActions:suggestedActions];
3606 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3608 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3609 @"Must create search web item in the tree.");
3610 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3611 @"Must create look up item in the tree.");
3612 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3613 @"Must create share item in the tree.");
3617- (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3618 if (@available(iOS 17.0, *)) {
3619 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3620 "https://github.com/flutter/flutter/issues/183473");
3623 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3625 [inputView setTextInputClient:123];
3626 [inputView reloadInputViews];
3627 [inputView becomeFirstResponder];
3628 XCTAssert(inputView.isFirstResponder);
3630 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3631 [NSNotificationCenter.defaultCenter
3632 postNotificationName:UIKeyboardWillShowNotification
3634 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3638 [textInputPlugin handleMethodCall:onPointerMoveCall
3639 result:^(id _Nullable result){
3641 XCTAssertFalse(inputView.isFirstResponder);
3645- (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3646 if (@available(iOS 17.0, *)) {
3647 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3648 "https://github.com/flutter/flutter/issues/183473");
3650 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3651 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3652 UIScene* scene = scenes.anyObject;
3653 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3654 UIWindowScene* windowScene = (UIWindowScene*)scene;
3655 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3656 UIWindow*
window = windowScene.windows[0];
3657 [window addSubview:viewController.view];
3659 [viewController loadView];
3662 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3664 [inputView setTextInputClient:123];
3665 [inputView reloadInputViews];
3666 [inputView becomeFirstResponder];
3669 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3670 [subView removeFromSuperview];
3674 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3675 [NSNotificationCenter.defaultCenter
3676 postNotificationName:UIKeyboardWillShowNotification
3678 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3682 [textInputPlugin handleMethodCall:onPointerMoveCall
3683 result:^(id _Nullable result){
3686 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3687 [subView removeFromSuperview];
3692- (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3693 if (@available(iOS 17.0, *)) {
3694 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3695 "https://github.com/flutter/flutter/issues/183473");
3697 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3698 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3699 UIScene* scene = scenes.anyObject;
3700 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3701 UIWindowScene* windowScene = (UIWindowScene*)scene;
3702 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3703 UIWindow*
window = windowScene.windows[0];
3704 [window addSubview:viewController.view];
3706 [viewController loadView];
3709 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3711 [inputView setTextInputClient:123];
3712 [inputView reloadInputViews];
3713 [inputView becomeFirstResponder];
3715 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3716 [NSNotificationCenter.defaultCenter
3717 postNotificationName:UIKeyboardWillShowNotification
3719 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3723 [textInputPlugin handleMethodCall:onPointerMoveCall
3724 result:^(id _Nullable result){
3728 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3733 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3734 result:^(id _Nullable result){
3738 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3740 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3741 [subView removeFromSuperview];
3746- (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3747 if (@available(iOS 17.0, *)) {
3748 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3749 "https://github.com/flutter/flutter/issues/183473");
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];
3763 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3765 [inputView setTextInputClient:123];
3766 [inputView reloadInputViews];
3767 [inputView becomeFirstResponder];
3769 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3770 [NSNotificationCenter.defaultCenter
3771 postNotificationName:UIKeyboardWillShowNotification
3773 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3777 [textInputPlugin handleMethodCall:onPointerMoveCall
3778 result:^(id _Nullable result){
3781 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3786 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3787 result:^(id _Nullable result){
3790 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3795 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3796 result:^(id _Nullable result){
3799 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3800 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3801 [subView removeFromSuperview];
3806- (void)testInteractiveKeyboardFindFirstResponderRecursive {
3808 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3809 [inputView setTextInputClient:123];
3810 [inputView reloadInputViews];
3811 [inputView becomeFirstResponder];
3813 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3814 XCTAssertEqualObjects(inputView, firstResponder);
3818- (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3825 [subInputView addSubview:subFirstResponderInputView];
3826 [inputView addSubview:subInputView];
3827 [inputView addSubview:otherSubInputView];
3828 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3829 [inputView setTextInputClient:123];
3830 [inputView reloadInputViews];
3831 [subInputView setTextInputClient:123];
3832 [subInputView reloadInputViews];
3833 [otherSubInputView setTextInputClient:123];
3834 [otherSubInputView reloadInputViews];
3835 [subFirstResponderInputView setTextInputClient:123];
3836 [subFirstResponderInputView reloadInputViews];
3837 [subFirstResponderInputView becomeFirstResponder];
3839 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3840 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3844- (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3846 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3847 [inputView setTextInputClient:123];
3848 [inputView reloadInputViews];
3850 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3851 XCTAssertNil(firstResponder);
3855- (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3856 if (@available(iOS 17.0, *)) {
3857 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3858 "https://github.com/flutter/flutter/issues/183473");
3860 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3861 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3862 UIScene* scene = scenes.anyObject;
3863 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3864 UIWindowScene* windowScene = (UIWindowScene*)scene;
3865 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3866 UIWindow*
window = windowScene.windows[0];
3867 [window addSubview:viewController.view];
3869 [viewController loadView];
3871 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3872 initWithDescription:
3873 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3874 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3875 .andDo(^(NSInvocation* invocation) {
3876 [expectation fulfill];
3878 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3879 [NSNotificationCenter.defaultCenter
3880 postNotificationName:UIKeyboardWillShowNotification
3882 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3886 [textInputPlugin handleMethodCall:initialMoveCall
3887 result:^(id _Nullable result){
3892 [textInputPlugin handleMethodCall:subsequentMoveCall
3893 result:^(id _Nullable result){
3899 [textInputPlugin handleMethodCall:pointerUpCall
3900 result:^(id _Nullable result){
3903 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3907- (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3908 if (@available(iOS 17.0, *)) {
3909 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3910 "https://github.com/flutter/flutter/issues/183473");
3912 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3913 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3914 UIScene* scene = scenes.anyObject;
3915 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3916 UIWindowScene* windowScene = (UIWindowScene*)scene;
3917 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3918 UIWindow*
window = windowScene.windows[0];
3919 [window addSubview:viewController.view];
3921 [viewController loadView];
3923 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3924 [NSNotificationCenter.defaultCenter
3925 postNotificationName:UIKeyboardWillShowNotification
3927 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3931 [textInputPlugin handleMethodCall:initialMoveCall
3932 result:^(id _Nullable result){
3937 [textInputPlugin handleMethodCall:subsequentMoveCall
3938 result:^(id _Nullable result){
3944 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3945 result:^(id _Nullable result){
3951 [textInputPlugin handleMethodCall:pointerUpCall
3952 result:^(id _Nullable result){
3954 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3955 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3957 XCTNSPredicateExpectation* expectation =
3958 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3959 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3963- (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3964 if (@available(iOS 17.0, *)) {
3965 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3966 "https://github.com/flutter/flutter/issues/183473");
3968 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3969 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3970 UIScene* scene = scenes.anyObject;
3971 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3972 UIWindowScene* windowScene = (UIWindowScene*)scene;
3973 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3974 UIWindow*
window = windowScene.windows[0];
3975 [window addSubview:viewController.view];
3977 [viewController loadView];
3980 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3982 [inputView setTextInputClient:123];
3983 [inputView reloadInputViews];
3984 [inputView becomeFirstResponder];
3986 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3987 [NSNotificationCenter.defaultCenter
3988 postNotificationName:UIKeyboardWillShowNotification
3990 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3994 [textInputPlugin handleMethodCall:initialMoveCall
3995 result:^(id _Nullable result){
4000 [textInputPlugin handleMethodCall:subsequentMoveCall
4001 result:^(id _Nullable result){
4007 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
4008 result:^(id _Nullable result){
4014 [textInputPlugin handleMethodCall:pointerUpCall
4015 result:^(id _Nullable result){
4017 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4018 return textInputPlugin.cachedFirstResponder.isFirstResponder;
4020 XCTNSPredicateExpectation* expectation =
4021 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4022 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4026- (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
4027 if (@available(iOS 17.0, *)) {
4028 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4029 "https://github.com/flutter/flutter/issues/183473");
4031 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4032 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4033 UIScene* scene = scenes.anyObject;
4034 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
4035 UIWindowScene* windowScene = (UIWindowScene*)scene;
4036 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4037 UIWindow*
window = windowScene.windows[0];
4038 [window addSubview:viewController.view];
4040 [viewController loadView];
4042 XCTestExpectation* expectation =
4043 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4044 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4045 [NSNotificationCenter.defaultCenter
4046 postNotificationName:UIKeyboardWillShowNotification
4048 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4052 [textInputPlugin handleMethodCall:initialMoveCall
4053 result:^(id _Nullable result){
4058 [textInputPlugin handleMethodCall:subsequentMoveCall
4059 result:^(id _Nullable result){
4064 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
4065 result:^(id _Nullable result){
4072 handleMethodCall:pointerUpCall
4073 result:^(id _Nullable result) {
4074 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4075 viewController.flutterScreenIfViewLoaded.bounds.size.height -
4076 keyboardFrame.origin.y);
4077 [expectation fulfill];
4082- (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
4083 if (@available(iOS 17.0, *)) {
4084 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4085 "https://github.com/flutter/flutter/issues/183473");
4087 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4088 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4089 UIScene* scene = scenes.anyObject;
4090 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
4091 UIWindowScene* windowScene = (UIWindowScene*)scene;
4092 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4093 UIWindow*
window = windowScene.windows[0];
4094 [window addSubview:viewController.view];
4096 [viewController loadView];
4098 XCTestExpectation* expectation =
4099 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4100 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4101 [NSNotificationCenter.defaultCenter
4102 postNotificationName:UIKeyboardWillShowNotification
4104 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4108 [textInputPlugin handleMethodCall:initialMoveCall
4109 result:^(id _Nullable result){
4114 [textInputPlugin handleMethodCall:subsequentMoveCall
4115 result:^(id _Nullable result){
4122 handleMethodCall:pointerUpCall
4123 result:^(id _Nullable result) {
4124 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4125 viewController.flutterScreenIfViewLoaded.bounds.size.height);
4126 [expectation fulfill];
4130- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
4131 [UIView setAnimationsEnabled:YES];
4132 [textInputPlugin showKeyboardAndRemoveScreenshot];
4134 UIView.areAnimationsEnabled,
4135 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
4138- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
4139 [UIView setAnimationsEnabled:YES];
4140 [textInputPlugin showKeyboardAndRemoveScreenshot];
4142 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4144 return UIView.areAnimationsEnabled;
4146 XCTNSPredicateExpectation* expectation =
4147 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4148 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4151- (void)testEditMenu_shouldCreateCustomMenuItemWithCorrectProperties {
4152 if (@available(iOS 16.0, *)) {
4157 [myViewController loadView];
4161 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4163 result:^(id _Nullable result){
4168 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4170 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4171 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4173 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4174 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4176 NSArray<NSDictionary*>* encodedItems = @[
4177 @{@"type" : @"custom", @"id" : @"custom-action-1", @"title" : @"Custom Action 1"},
4178 @{@"type" : @"custom", @"id" : @"custom-action-2", @"title" : @"Custom Action 2"},
4181 BOOL shownEditMenu =
4182 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4183 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4185 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4186 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4187 suggestedActions:@[]];
4189 XCTAssertEqual(menu.children.count, 2UL,
@"Should create 2 custom menu items");
4190 UIAction* firstAction = (UIAction*)menu.children[0];
4191 UIAction* secondAction = (UIAction*)menu.children[1];
4192 XCTAssertEqualObjects(firstAction.title,
@"Custom Action 1",
4193 @"First action title should match");
4194 XCTAssertEqualObjects(secondAction.title,
@"Custom Action 2",
4195 @"Second action title should match");
4199- (void)testEditMenu_customActionShouldTriggerDelegateCallback {
4200 if (@available(iOS 16.0, *)) {
4203 OCMStub([mockEngine platformChannel]).andReturn(mockPlatformChannel);
4205 OCMStub([mockEngine flutterTextInputView:[OCMArg any]
4206 performContextMenuCustomActionWithActionID:
@"test-callback-id"
4207 textInputClient:123])
4208 .andDo((^(NSInvocation* invocation) {
4209 [mockPlatformChannel invokeMethod:@"ContextMenu.onPerformCustomAction"
4210 arguments:@[ @(123), @"test-callback-id" ]];
4217 [myViewController loadView];
4221 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4223 result:^(id _Nullable result){
4228 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4229 XCTestExpectation* expectation = [[XCTestExpectation alloc]
4230 initWithDescription:@"Custom action delegate callback should be called"];
4231 OCMStub(([mockPlatformChannel invokeMethod:
@"ContextMenu.onPerformCustomAction"
4232 arguments:@[ @(123),
@"test-callback-id" ]]))
4233 .andDo(^(NSInvocation* invocation) {
4234 [expectation fulfill];
4236 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4237 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4239 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4240 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4242 NSArray<NSDictionary*>* encodedItems = @[
4243 @{@"type" : @"custom", @"id" : @"test-callback-id", @"title" : @"Test Action"},
4246 BOOL shownEditMenu =
4247 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4248 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4250 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4251 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4252 suggestedActions:@[]];
4254 XCTAssertEqual(menu.children.count, 1UL,
@"Should have 1 custom menu item");
4255 UIAction* customAction = (UIAction*)menu.children[0];
4256 XCTAssertEqualObjects(customAction.title,
@"Test Action",
@"Action title should match");
4258 [myInputView.textInputDelegate flutterTextInputView:myInputView
4259 performContextMenuCustomActionWithActionID:@"test-callback-id"
4260 textInputClient:123];
4262 [
self waitForExpectations:@[ expectation ] timeout:1.0];
4263 OCMVerifyAll(mockPlatformChannel);
AssetResolverType
Identifies the type of AssetResolver an instance is.
A Mapping like NonOwnedMapping, but uses Free as its release proc.
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
G_BEGIN_DECLS FlutterViewId view_id
FlutterDesktopBinaryReply callback
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
void flutterTextInputView:performAction:withClient:(FlutterTextInputView *textInputView,[performAction] FlutterTextInputAction action,[withClient] int client)
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
FlutterViewController * viewController
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)
NSArray< FlutterTextSelectionRect * > * selectionRects
UITextRange * markedTextRange
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
CGRect caretRectForPosition
const CGRect kInvalidFirstRect
NSDictionary * _passwordTemplate
FlutterViewController * viewController
FlutterTextInputPlugin * textInputPlugin
std::function< void()> closure
impeller::ShaderType type