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,
128 std::shared_ptr<fml::BasicTaskRunner> OnPlatformViewGetShutdownSafeIOTaskRunner()
const override {
142 NSDictionary* _template;
160 UIPasteboard.generalPasteboard.items = @[];
166 [textInputPlugin.autofillContext removeAllObjects];
167 [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
168 [[[[textInputPlugin textInputView] superview] subviews]
169 makeObjectsPerformSelector:@selector(removeFromSuperview)];
174- (void)setClientId:(
int)clientId configuration:(NSDictionary*)config {
177 arguments:@[ [NSNumber numberWithInt:clientId], config ]];
178 [textInputPlugin handleMethodCall:setClientCall
179 result:^(id _Nullable result){
183- (void)setClientClear {
186 [textInputPlugin handleMethodCall:clearClientCall
187 result:^(id _Nullable result){
191- (void)setTextInputShow {
194 [textInputPlugin handleMethodCall:setClientCall
195 result:^(id _Nullable result){
199- (void)setTextInputHide {
202 [textInputPlugin handleMethodCall:setClientCall
203 result:^(id _Nullable result){
207- (void)flushScheduledAsyncBlocks {
208 __block
bool done =
false;
209 XCTestExpectation* expectation =
210 [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
211 dispatch_async(dispatch_get_main_queue(), ^{
214 dispatch_async(dispatch_get_main_queue(), ^{
216 [expectation fulfill];
218 [
self waitForExpectations:@[ expectation ] timeout:10];
221- (NSMutableDictionary*)mutableTemplateCopy {
224 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
225 @"keyboardAppearance" :
@"Brightness.light",
226 @"obscureText" : @NO,
227 @"inputAction" :
@"TextInputAction.unspecified",
228 @"smartDashesType" :
@"0",
229 @"smartQuotesType" :
@"0",
230 @"autocorrect" : @YES,
231 @"enableInteractiveSelection" : @YES,
235 return [_template mutableCopy];
239 return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
240 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
244- (
FlutterTextRange*)getLineRangeFromTokenizer:(
id<UITextInputTokenizer>)tokenizer
245 atIndex:(NSInteger)index {
248 withGranularity:UITextGranularityLine
249 inDirection:UITextLayoutDirectionRight];
254- (void)updateConfig:(NSDictionary*)config {
257 [textInputPlugin handleMethodCall:updateConfigCall
258 result:^(id _Nullable result){
264- (void)testWillNotCrashWhenViewControllerIsNil {
271 XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
274 result:^(id _Nullable result) {
275 XCTAssertNil(result);
276 [expectation fulfill];
278 XCTAssertNil(inputPlugin.activeView);
279 [
self waitForExpectations:@[ expectation ] timeout:1.0];
282- (void)testInvokeStartLiveTextInput {
287 result:^(id _Nullable result){
289 OCMVerify([mockPlugin startLiveTextInput]);
292- (void)testNoDanglingEnginePointer {
302 weakFlutterEngine = flutterEngine;
303 XCTAssertNotNil(weakFlutterEngine,
@"flutter engine must not be nil");
305 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
306 weakFlutterTextInputPlugin = flutterTextInputPlugin;
310 NSDictionary* config =
self.mutableTemplateCopy;
313 arguments:@[ [NSNumber numberWithInt:123], config ]];
315 result:^(id _Nullable result){
317 currentView = flutterTextInputPlugin.activeView;
320 XCTAssertNil(weakFlutterEngine,
@"flutter engine must be nil");
321 XCTAssertNotNil(currentView,
@"current view must not be nil");
323 XCTAssertNil(weakFlutterTextInputPlugin);
326 XCTAssertNil(currentView.textInputDelegate);
329- (void)testSecureInput {
330 NSDictionary* config =
self.mutableTemplateCopy;
331 [config setValue:@"YES" forKey:@"obscureText"];
332 [
self setClientId:123 configuration:config];
335 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
342 XCTAssertTrue(inputView.secureTextEntry);
345 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
348 XCTAssertEqual(inputFields.count, 1ul);
356 XCTAssert(inputView.autofillId.length > 0);
359- (void)testKeyboardType {
360 NSDictionary* config =
self.mutableTemplateCopy;
361 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
362 [
self setClientId:123 configuration:config];
365 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
370 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
373- (void)testKeyboardTypeWebSearch {
374 NSDictionary* config =
self.mutableTemplateCopy;
375 [config setValue:@{@"name" : @"TextInputType.webSearch"} forKey:@"inputType"];
376 [
self setClientId:123 configuration:config];
379 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
384 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeWebSearch);
387- (void)testKeyboardTypeTwitter {
388 NSDictionary* config =
self.mutableTemplateCopy;
389 [config setValue:@{@"name" : @"TextInputType.twitter"} forKey:@"inputType"];
390 [
self setClientId:123 configuration:config];
393 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
398 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeTwitter);
401- (void)testVisiblePasswordUseAlphanumeric {
402 NSDictionary* config =
self.mutableTemplateCopy;
403 [config setValue:@{@"name" : @"TextInputType.visiblePassword"} forKey:@"inputType"];
404 [
self setClientId:123 configuration:config];
407 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
412 XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeASCIICapable);
415- (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
416 NSDictionary* config =
self.mutableTemplateCopy;
417 [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
418 [
self setClientId:123 configuration:config];
423 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
424 [
self setClientId:124 configuration:config];
429- (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
433 if (@available(iOS 17.0, *)) {
435 OCMVerify(never(), [
engine flutterTextInputView:inputView
436 showAutocorrectionPromptRectForStart:0
440 OCMVerify([
engine flutterTextInputView:inputView
441 showAutocorrectionPromptRectForStart:0
447- (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
449 __block
int updateCount = 0;
450 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
451 .andDo(^(NSInvocation* invocation) {
455 [inputView.text setString:@"Some initial text"];
456 XCTAssertEqual(updateCount, 0);
459 [inputView setSelectedTextRange:textRange];
460 XCTAssertEqual(updateCount, 1);
463 NSDictionary* config =
self.mutableTemplateCopy;
464 [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
465 [config setValue:@(NO) forKey:@"obscureText"];
466 [config setValue:@(NO) forKey:@"enableDeltaModel"];
467 [inputView configureWithDictionary:config];
470 [inputView setSelectedTextRange:textRange];
472 XCTAssertEqual(updateCount, 1);
475- (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
477 if (@available(iOS 17.0, *)) {
481 if (@available(iOS 14.0, *)) {
484 __block
int callCount = 0;
485 OCMStub([
engine flutterTextInputView:inputView
486 showAutocorrectionPromptRectForStart:0
489 .andDo(^(NSInvocation* invocation) {
495 XCTAssertEqual(callCount, 1);
497 UIScribbleInteraction* scribbleInteraction =
498 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
500 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
504 XCTAssertEqual(callCount, 1);
506 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
507 [inputView resetScribbleInteractionStatusIfEnding];
510 XCTAssertEqual(callCount, 2);
512 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
516 XCTAssertEqual(callCount, 2);
518 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
522 XCTAssertEqual(callCount, 2);
524 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
527 XCTAssertEqual(callCount, 3);
531- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
537 arguments:@[ @(123),
self.mutableTemplateCopy ]];
539 result:^(id _Nullable result){
546 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
550 arguments:@{@"transform" : yOffsetMatrix}];
552 result:^(id _Nullable result){
555 if (@available(iOS 17, *)) {
556 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
557 @"The input hider should overlap with the text on and after iOS 17");
560 XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
561 @"The input hider should be on the origin of screen on and before iOS 16.");
565- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
571 toPosition:toPosition];
572 NSRange range = flutterRange.
range;
574 XCTAssertEqual(range.location, 0ul);
575 XCTAssertEqual(range.length, 2ul);
578- (void)testTextInRange {
579 NSDictionary* config =
self.mutableTemplateCopy;
580 [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
581 [
self setClientId:123 configuration:config];
582 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
585 [inputView insertText:@"test"];
588 NSString* substring = [inputView textInRange:range];
589 XCTAssertEqual(substring.length, 4ul);
592 substring = [inputView textInRange:range];
593 XCTAssertEqual(substring.length, 0ul);
596- (void)testTextInRangeAcceptsNSNotFoundLocationGracefully {
597 NSDictionary* config =
self.mutableTemplateCopy;
598 [
self setClientId:123 configuration:config];
599 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
602 [inputView insertText:@"text"];
605 NSString* substring = [inputView textInRange:range];
606 XCTAssertNil(substring);
609- (void)testStandardEditActions {
610 NSDictionary* config =
self.mutableTemplateCopy;
611 [
self setClientId:123 configuration:config];
612 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
615 [inputView insertText:@"aaaa"];
616 [inputView selectAll:nil];
618 [inputView insertText:@"bbbb"];
619 XCTAssertTrue([inputView canPerformAction:@selector(paste:) withSender:nil]);
620 [inputView paste:nil];
621 [inputView selectAll:nil];
622 [inputView copy:nil];
623 [inputView paste:nil];
624 [inputView selectAll:nil];
625 [inputView delete:nil];
626 [inputView paste:nil];
627 [inputView paste:nil];
630 NSString* substring = [inputView textInRange:range];
631 XCTAssertEqualObjects(substring,
@"bbbbaaaabbbbaaaa");
634- (void)testCanPerformActionForSelectActions {
635 NSDictionary* config =
self.mutableTemplateCopy;
636 [
self setClientId:123 configuration:config];
637 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
640 XCTAssertFalse([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
642 [inputView insertText:@"aaaa"];
644 XCTAssertTrue([inputView canPerformAction:@selector(selectAll:) withSender:nil]);
647- (void)testCanPerformActionCaptureTextFromCamera {
648 if (@available(iOS 15.0, *)) {
649 NSDictionary* config =
self.mutableTemplateCopy;
650 [
self setClientId:123 configuration:config];
651 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
654 [inputView becomeFirstResponder];
655 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
657 [inputView insertText:@"test"];
658 [inputView selectAll:nil];
659 XCTAssertTrue([inputView canPerformAction:@selector(captureTextFromCamera:) withSender:nil]);
663- (void)testDeletingBackward {
664 NSDictionary* config =
self.mutableTemplateCopy;
665 [
self setClientId:123 configuration:config];
666 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
669 [inputView insertText:@"ឹ😀 text 🥰👨👩👧👦🇺🇳ดี "];
670 [inputView deleteBackward];
671 [inputView deleteBackward];
674 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳ด");
675 [inputView deleteBackward];
676 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦🇺🇳");
677 [inputView deleteBackward];
678 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰👨👩👧👦");
679 [inputView deleteBackward];
680 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text 🥰");
681 [inputView deleteBackward];
683 XCTAssertEqualObjects(inputView.text,
@"ឹ😀 text ");
684 [inputView deleteBackward];
685 [inputView deleteBackward];
686 [inputView deleteBackward];
687 [inputView deleteBackward];
688 [inputView deleteBackward];
689 [inputView deleteBackward];
691 XCTAssertEqualObjects(inputView.text,
@"ឹ😀");
692 [inputView deleteBackward];
693 XCTAssertEqualObjects(inputView.text,
@"ឹ");
694 [inputView deleteBackward];
695 XCTAssertEqualObjects(inputView.text,
@"");
700- (void)testSystemOnlyAddingPartialComposedCharacter {
701 NSDictionary* config =
self.mutableTemplateCopy;
702 [
self setClientId:123 configuration:config];
703 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
706 [inputView insertText:@"👨👩👧👦"];
707 [inputView deleteBackward];
710 [inputView insertText:[@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)]];
711 [inputView insertText:@"아"];
713 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦아");
716 [inputView deleteBackward];
719 [inputView insertText:@"😀"];
720 [inputView deleteBackward];
722 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
723 [inputView insertText:@"아"];
724 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
727 [inputView deleteBackward];
730 [inputView deleteBackward];
732 [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
733 [inputView insertText:@"아"];
735 XCTAssertEqualObjects(inputView.text,
@"👨👩👧👦😀아");
738- (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
739 NSDictionary* config =
self.mutableTemplateCopy;
740 [
self setClientId:123 configuration:config];
741 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
744 [inputView insertText:@"👨👩👧👦"];
745 [inputView deleteBackward];
746 [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
749 NSString* brokenEmoji = [@"👨👩👧👦" substringWithRange:NSMakeRange(0, 1)];
750 [inputView insertText:brokenEmoji];
751 [inputView insertText:@"아"];
753 NSString* finalText = [NSString stringWithFormat:@"%@아", brokenEmoji];
754 XCTAssertEqualObjects(inputView.text, finalText);
757- (void)testPastingNonTextDisallowed {
758 NSDictionary* config =
self.mutableTemplateCopy;
759 [
self setClientId:123 configuration:config];
760 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
763 UIPasteboard.generalPasteboard.color = UIColor.redColor;
764 XCTAssertNil(UIPasteboard.generalPasteboard.string);
765 XCTAssertFalse([inputView canPerformAction:@selector(paste:) withSender:nil]);
766 [inputView paste:nil];
768 XCTAssertEqualObjects(inputView.text,
@"");
771- (void)testNoZombies {
778 [passwordView.textField description];
780 XCTAssert([[passwordView.
textField description] containsString:
@"TextField"]);
783- (void)testInputViewCrash {
788 initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
789 activeView = inputPlugin.activeView;
791 [activeView updateEditingState];
794- (void)testDoNotReuseInputViews {
795 NSDictionary* config =
self.mutableTemplateCopy;
796 [
self setClientId:123 configuration:config];
798 [
self setClientId:456 configuration:config];
800 XCTAssertNotNil(currentView);
805- (void)ensureOnlyActiveViewCanBecomeFirstResponder {
807 XCTAssertEqual(inputView.canBecomeFirstResponder, inputView ==
textInputPlugin.activeView);
811- (void)testPropagatePressEventsToViewController {
813 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
814 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
818 NSDictionary* config =
self.mutableTemplateCopy;
819 [
self setClientId:123 configuration:config];
821 [
self setTextInputShow];
823 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
824 withEvent:OCMClassMock([UIPressesEvent class])];
826 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
827 withEvent:[OCMArg isNotNil]]);
828 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
829 withEvent:[OCMArg isNotNil]]);
831 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
832 withEvent:OCMClassMock([UIPressesEvent class])];
834 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
835 withEvent:[OCMArg isNotNil]]);
836 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
837 withEvent:[OCMArg isNotNil]]);
840- (void)testPropagatePressEventsToViewController2 {
842 OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
843 OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
847 NSDictionary* config =
self.mutableTemplateCopy;
848 [
self setClientId:123 configuration:config];
849 [
self setTextInputShow];
852 [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
853 withEvent:OCMClassMock([UIPressesEvent class])];
855 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
856 withEvent:[OCMArg isNotNil]]);
857 OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
858 withEvent:[OCMArg isNotNil]]);
861 [
self setClientId:321 configuration:config];
862 [
self setTextInputShow];
864 NSAssert(
textInputPlugin.activeView != currentView,
@"active view must change");
866 [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
867 withEvent:OCMClassMock([UIPressesEvent class])];
869 OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
870 withEvent:[OCMArg isNotNil]]);
871 OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
872 withEvent:[OCMArg isNotNil]]);
875- (void)testHotRestart {
876 flutter::MockPlatformViewDelegate mock_platform_view_delegate;
877 auto thread = std::make_unique<fml::Thread>(
"TextInputHotRestart");
878 auto thread_task_runner = thread->GetTaskRunner();
884 id mockFlutterView = OCMClassMock([
FlutterView class]);
887 OCMStub([mockFlutterViewController viewIfLoaded]).andReturn(mockFlutterView);
888 OCMStub([mockFlutterViewController
textInputPlugin]).andReturn(mockFlutterTextInputPlugin);
891 thread_task_runner->PostTask([&] {
892 auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
893 mock_platform_view_delegate,
894 mock_platform_view_delegate.settings_.enable_impeller
899 std::make_shared<
fml::SyncSwitch>());
901 platform_view->SetOwnerViewController(mockFlutterViewController);
903 OCMExpect([mockFlutterTextInputPlugin reset]);
905 OCMVerifyAll(mockFlutterView);
912- (void)testUpdateSecureTextEntry {
913 NSDictionary* config =
self.mutableTemplateCopy;
914 [config setValue:@"YES" forKey:@"obscureText"];
915 [
self setClientId:123 configuration:config];
917 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
920 __block
int callCount = 0;
921 OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
925 XCTAssertTrue(inputView.isSecureTextEntry);
927 config =
self.mutableTemplateCopy;
928 [config setValue:@"NO" forKey:@"obscureText"];
929 [
self updateConfig:config];
931 XCTAssertEqual(callCount, 1);
932 XCTAssertFalse(inputView.isSecureTextEntry);
935- (void)testInputActionContinueAction {
951 arguments:@[ @(123), @"TextInputAction.continueAction" ]];
953 OCMVerify([mockBinaryMessenger sendOnChannel:
@"flutter/textinput" message:encodedMethodCall]);
956- (void)testDisablingAutocorrectDisablesSpellChecking {
960 NSDictionary* config =
self.mutableTemplateCopy;
961 [inputView configureWithDictionary:config];
963 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
964 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
966 [config setValue:@(NO) forKey:@"autocorrect"];
967 [inputView configureWithDictionary:config];
969 XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
970 XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
973- (void)testEnableInlinePredictionFromConfiguration
API_AVAILABLE(ios(17.0)) {
975 NSMutableDictionary* config =
self.mutableTemplateCopy;
978 [inputView configureWithDictionary:config];
979 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
981 [config setValue:@NO forKey:@"enableInlinePrediction"];
982 [inputView configureWithDictionary:config];
983 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
985 [config setValue:@YES forKey:@"enableInlinePrediction"];
986 [inputView configureWithDictionary:config];
987 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeYes);
990 [config removeObjectForKey:@"enableInlinePrediction"];
991 [inputView configureWithDictionary:config];
992 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
995 [config setValue:[NSNull null] forKey:@"enableInlinePrediction"];
996 [inputView configureWithDictionary:config];
997 XCTAssertEqual(inputView.inlinePredictionType, UITextInlinePredictionTypeNo);
1000- (void)testReplaceTestLocalAdjustSelectionAndMarkedTextRange {
1002 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1016 XCTAssertEqual(inputView.markedTextRange, nil);
1019- (void)testFlutterTextInputViewIsNotClearWhenKeyboardShowAndHide {
1022 [inputView setMarkedText:@"test text" selectedRange:NSMakeRange(0, 5)];
1023 XCTAssertEqualObjects(inputView.text,
@"test text");
1026 [
self setTextInputShow];
1027 XCTAssertEqualObjects(inputView.text,
@"test text");
1030 [
self setTextInputHide];
1031 XCTAssertEqualObjects(inputView.text,
@"test text");
1034- (void)testFlutterTextInputViewOnlyRespondsToInsertionPointColorBelowIOS17 {
1038 SEL insertionPointColor = NSSelectorFromString(
@"insertionPointColor");
1039 BOOL respondsToInsertionPointColor = [inputView respondsToSelector:insertionPointColor];
1040 if (@available(iOS 17, *)) {
1041 XCTAssertFalse(respondsToInsertionPointColor);
1043 XCTAssertTrue(respondsToInsertionPointColor);
1047- (void)testSetAttributedMarkedTextSelectedRange
API_AVAILABLE(ios(17.0)) {
1049 NSAttributedString* attributedText =
1050 [[NSAttributedString alloc] initWithString:@"inline prediction"
1052 NSForegroundColorAttributeName : [UIColor grayColor],
1054 [inputView setAttributedMarkedText:attributedText selectedRange:NSMakeRange(0, 7)];
1056 XCTAssertEqualObjects(inputView.text,
@"inline prediction");
1057 NSRange selectedRange = ((
FlutterTextRange*)inputView.selectedTextRange).range;
1058 XCTAssertEqual(selectedRange.location, 0ul);
1059 XCTAssertEqual(selectedRange.length, 7ul);
1061 XCTAssertNotNil(markedRange);
1062 XCTAssertEqual(markedRange.range.location, 0ul);
1064 XCTAssertEqual(markedRange.range.length, 17ul);
1067 [inputView setAttributedMarkedText:nil selectedRange:NSMakeRange(0, 0)];
1068 XCTAssertEqualObjects(inputView.text,
@"");
1069 XCTAssertNil(inputView.markedTextRange);
1072#pragma mark - TextEditingDelta tests
1073- (void)testTextEditingDeltasAreGeneratedOnTextInput {
1075 inputView.enableDeltaModel = YES;
1077 __block
int updateCount = 0;
1079 [inputView insertText:@"text to insert"];
1082 flutterTextInputView:inputView
1083 updateEditingClient:0
1084 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1085 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1086 isEqualToString:
@""]) &&
1087 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1088 isEqualToString:
@"text to insert"]) &&
1089 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1090 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 0);
1092 .andDo(^(NSInvocation* invocation) {
1095 XCTAssertEqual(updateCount, 0);
1097 [
self flushScheduledAsyncBlocks];
1100 XCTAssertEqual(updateCount, 1);
1102 [inputView deleteBackward];
1103 OCMExpect([
engine flutterTextInputView:inputView
1104 updateEditingClient:0
1105 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1106 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1107 isEqualToString:
@"text to insert"]) &&
1108 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1109 isEqualToString:
@""]) &&
1110 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1112 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1115 .andDo(^(NSInvocation* invocation) {
1118 [
self flushScheduledAsyncBlocks];
1119 XCTAssertEqual(updateCount, 2);
1122 OCMExpect([
engine flutterTextInputView:inputView
1123 updateEditingClient:0
1124 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1125 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1126 isEqualToString:
@"text to inser"]) &&
1127 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1128 isEqualToString:
@""]) &&
1129 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1131 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1134 .andDo(^(NSInvocation* invocation) {
1137 [
self flushScheduledAsyncBlocks];
1138 XCTAssertEqual(updateCount, 3);
1141 withText:@"replace text"];
1144 flutterTextInputView:inputView
1145 updateEditingClient:0
1146 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1147 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1148 isEqualToString:
@"text to inser"]) &&
1149 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1150 isEqualToString:
@"replace text"]) &&
1151 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 0) &&
1152 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 1);
1154 .andDo(^(NSInvocation* invocation) {
1157 [
self flushScheduledAsyncBlocks];
1158 XCTAssertEqual(updateCount, 4);
1160 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1161 OCMExpect([
engine flutterTextInputView:inputView
1162 updateEditingClient:0
1163 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1164 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1165 isEqualToString:
@"replace textext to inser"]) &&
1166 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1167 isEqualToString:
@"marked text"]) &&
1168 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"]
1170 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"]
1173 .andDo(^(NSInvocation* invocation) {
1176 [
self flushScheduledAsyncBlocks];
1177 XCTAssertEqual(updateCount, 5);
1179 [inputView unmarkText];
1181 flutterTextInputView:inputView
1182 updateEditingClient:0
1183 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1184 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1185 isEqualToString:
@"replace textmarked textext to inser"]) &&
1186 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1187 isEqualToString:
@""]) &&
1188 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] ==
1190 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] ==
1193 .andDo(^(NSInvocation* invocation) {
1196 [
self flushScheduledAsyncBlocks];
1198 XCTAssertEqual(updateCount, 6);
1202- (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
1205 inputView.enableDeltaModel = YES;
1208 OCMExpect([
engine flutterTextInputView:inputView
1209 updateEditingClient:0
1210 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1211 NSArray* deltas = state[@"deltas"];
1212 NSDictionary* firstDelta = deltas[0];
1213 NSDictionary* secondDelta = deltas[1];
1214 NSDictionary* thirdDelta = deltas[2];
1215 return [firstDelta[@"oldText"] isEqualToString:@""] &&
1216 [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
1217 [firstDelta[@"deltaStart"] intValue] == 0 &&
1218 [firstDelta[@"deltaEnd"] intValue] == 0 &&
1219 [secondDelta[@"oldText"] isEqualToString:@"-"] &&
1220 [secondDelta[@"deltaText"] isEqualToString:@""] &&
1221 [secondDelta[@"deltaStart"] intValue] == 0 &&
1222 [secondDelta[@"deltaEnd"] intValue] == 1 &&
1223 [thirdDelta[@"oldText"] isEqualToString:@""] &&
1224 [thirdDelta[@"deltaText"] isEqualToString:@"—"] &&
1225 [thirdDelta[@"deltaStart"] intValue] == 0 &&
1226 [thirdDelta[@"deltaEnd"] intValue] == 0;
1230 [inputView insertText:@"-"];
1231 [inputView deleteBackward];
1232 [inputView insertText:@"—"];
1234 [
self flushScheduledAsyncBlocks];
1238- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
1240 inputView.enableDeltaModel = YES;
1242 __block
int updateCount = 0;
1243 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1244 .andDo(^(NSInvocation* invocation) {
1248 [inputView.text setString:@"Some initial text"];
1249 XCTAssertEqual(updateCount, 0);
1252 inputView.markedTextRange = range;
1253 inputView.selectedTextRange = nil;
1254 [
self flushScheduledAsyncBlocks];
1255 XCTAssertEqual(updateCount, 1);
1257 [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
1259 flutterTextInputView:inputView
1260 updateEditingClient:0
1261 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1262 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1263 isEqualToString:
@"Some initial text"]) &&
1264 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1265 isEqualToString:
@"new marked text."]) &&
1266 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1267 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1269 [
self flushScheduledAsyncBlocks];
1270 XCTAssertEqual(updateCount, 2);
1273- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
1275 inputView.enableDeltaModel = YES;
1277 __block
int updateCount = 0;
1278 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1279 .andDo(^(NSInvocation* invocation) {
1283 [inputView.text setString:@"Some initial text"];
1284 [
self flushScheduledAsyncBlocks];
1285 XCTAssertEqual(updateCount, 0);
1288 inputView.markedTextRange = range;
1289 inputView.selectedTextRange = nil;
1290 [
self flushScheduledAsyncBlocks];
1291 XCTAssertEqual(updateCount, 1);
1293 [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1295 flutterTextInputView:inputView
1296 updateEditingClient:0
1297 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1298 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1299 isEqualToString:
@"Some initial text"]) &&
1300 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1301 isEqualToString:
@"text."]) &&
1302 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1303 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1305 [
self flushScheduledAsyncBlocks];
1306 XCTAssertEqual(updateCount, 2);
1309- (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1311 inputView.enableDeltaModel = YES;
1313 __block
int updateCount = 0;
1314 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1315 .andDo(^(NSInvocation* invocation) {
1319 [inputView.text setString:@"Some initial text"];
1320 [
self flushScheduledAsyncBlocks];
1321 XCTAssertEqual(updateCount, 0);
1324 inputView.markedTextRange = range;
1325 inputView.selectedTextRange = nil;
1326 [
self flushScheduledAsyncBlocks];
1327 XCTAssertEqual(updateCount, 1);
1329 [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1331 flutterTextInputView:inputView
1332 updateEditingClient:0
1333 withDelta:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1334 return ([[state[
@"deltas"] objectAtIndex:0][
@"oldText"]
1335 isEqualToString:
@"Some initial text"]) &&
1336 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaText"]
1337 isEqualToString:
@"tex"]) &&
1338 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaStart"] intValue] == 13) &&
1339 ([[state[
@"deltas"] objectAtIndex:0][
@"deltaEnd"] intValue] == 17);
1341 [
self flushScheduledAsyncBlocks];
1342 XCTAssertEqual(updateCount, 2);
1345#pragma mark - EditingState tests
1347- (void)testUITextInputCallsUpdateEditingStateOnce {
1350 __block
int updateCount = 0;
1351 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1352 .andDo(^(NSInvocation* invocation) {
1356 [inputView insertText:@"text to insert"];
1358 XCTAssertEqual(updateCount, 1);
1360 [inputView deleteBackward];
1361 XCTAssertEqual(updateCount, 2);
1364 XCTAssertEqual(updateCount, 3);
1367 withText:@"replace text"];
1368 XCTAssertEqual(updateCount, 4);
1370 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1371 XCTAssertEqual(updateCount, 5);
1373 [inputView unmarkText];
1374 XCTAssertEqual(updateCount, 6);
1377- (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1379 inputView.enableDeltaModel = YES;
1381 __block
int updateCount = 0;
1382 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1383 .andDo(^(NSInvocation* invocation) {
1387 [inputView insertText:@"text to insert"];
1388 [
self flushScheduledAsyncBlocks];
1390 XCTAssertEqual(updateCount, 1);
1392 [inputView deleteBackward];
1393 [
self flushScheduledAsyncBlocks];
1394 XCTAssertEqual(updateCount, 2);
1397 [
self flushScheduledAsyncBlocks];
1398 XCTAssertEqual(updateCount, 3);
1401 withText:@"replace text"];
1402 [
self flushScheduledAsyncBlocks];
1403 XCTAssertEqual(updateCount, 4);
1405 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1406 [
self flushScheduledAsyncBlocks];
1407 XCTAssertEqual(updateCount, 5);
1409 [inputView unmarkText];
1410 [
self flushScheduledAsyncBlocks];
1411 XCTAssertEqual(updateCount, 6);
1414- (void)testTextChangesDoNotTriggerUpdateEditingClient {
1417 __block
int updateCount = 0;
1418 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1419 .andDo(^(NSInvocation* invocation) {
1423 [inputView.text setString:@"BEFORE"];
1424 XCTAssertEqual(updateCount, 0);
1426 inputView.markedTextRange = nil;
1427 inputView.selectedTextRange = nil;
1428 XCTAssertEqual(updateCount, 1);
1431 XCTAssertEqual(updateCount, 1);
1432 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1433 XCTAssertEqual(updateCount, 1);
1434 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1435 XCTAssertEqual(updateCount, 1);
1439 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1440 XCTAssertEqual(updateCount, 1);
1442 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1443 XCTAssertEqual(updateCount, 1);
1447 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1448 XCTAssertEqual(updateCount, 1);
1450 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1451 XCTAssertEqual(updateCount, 1);
1454- (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1456 inputView.enableDeltaModel = YES;
1458 __block
int updateCount = 0;
1459 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1460 .andDo(^(NSInvocation* invocation) {
1464 [inputView.text setString:@"BEFORE"];
1465 [
self flushScheduledAsyncBlocks];
1466 XCTAssertEqual(updateCount, 0);
1468 inputView.markedTextRange = nil;
1469 inputView.selectedTextRange = nil;
1470 [
self flushScheduledAsyncBlocks];
1471 XCTAssertEqual(updateCount, 1);
1474 XCTAssertEqual(updateCount, 1);
1475 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1476 [
self flushScheduledAsyncBlocks];
1477 XCTAssertEqual(updateCount, 1);
1479 [inputView setTextInputState:@{@"text" : @"AFTER"}];
1480 [
self flushScheduledAsyncBlocks];
1481 XCTAssertEqual(updateCount, 1);
1485 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1486 [
self flushScheduledAsyncBlocks];
1487 XCTAssertEqual(updateCount, 1);
1490 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1491 [
self flushScheduledAsyncBlocks];
1492 XCTAssertEqual(updateCount, 1);
1496 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1497 [
self flushScheduledAsyncBlocks];
1498 XCTAssertEqual(updateCount, 1);
1501 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1502 [
self flushScheduledAsyncBlocks];
1503 XCTAssertEqual(updateCount, 1);
1506- (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1509 __block
int updateCount = 0;
1510 OCMStub([
engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1511 .andDo(^(NSInvocation* invocation) {
1515 [inputView unmarkText];
1517 XCTAssertEqual(updateCount, 0);
1519 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1521 XCTAssertEqual(updateCount, 1);
1523 [inputView unmarkText];
1525 XCTAssertEqual(updateCount, 2);
1528- (void)testCanCopyPasteWithScribbleEnabled {
1529 if (@available(iOS 14.0, *)) {
1530 NSDictionary* config =
self.mutableTemplateCopy;
1531 [
self setClientId:123 configuration:config];
1532 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
1538 [mockInputView insertText:@"aaaa"];
1539 [mockInputView selectAll:nil];
1541 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1542 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1543 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1544 XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1546 [mockInputView copy:NULL];
1547 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1548 XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:
@"sender"]);
1549 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1550 XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:
@"sender"]);
1554- (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1555 if (@available(iOS 14.0, *)) {
1558 __block
int updateCount = 0;
1559 OCMStub([
engine flutterTextInputView:inputView
1560 updateEditingClient:0
1561 withState:[OCMArg isNotNil]])
1562 .andDo(^(NSInvocation* invocation) {
1566 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1568 XCTAssertEqual(updateCount, 1);
1570 UIScribbleInteraction* scribbleInteraction =
1571 [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1573 [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1574 [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1576 XCTAssertEqual(updateCount, 1);
1578 [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1579 [inputView resetScribbleInteractionStatusIfEnding];
1580 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1582 XCTAssertEqual(updateCount, 2);
1584 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1585 [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1588 XCTAssertEqual(updateCount, 2);
1590 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1591 [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1594 XCTAssertEqual(updateCount, 2);
1596 inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1597 [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1599 XCTAssertEqual(updateCount, 3);
1603- (void)testUpdateEditingClientNegativeSelection {
1606 [inputView.text setString:@"SELECTION"];
1607 inputView.markedTextRange = nil;
1608 inputView.selectedTextRange = nil;
1610 [inputView setTextInputState:@{
1611 @"text" : @"SELECTION",
1612 @"selectionBase" : @-1,
1613 @"selectionExtent" : @-1
1615 [inputView updateEditingState];
1616 OCMVerify([
engine flutterTextInputView:inputView
1617 updateEditingClient:0
1618 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1619 return ([state[
@"selectionBase"] intValue]) == 0 &&
1620 ([state[@"selectionExtent"] intValue] == 0);
1625 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1626 [inputView updateEditingState];
1627 OCMVerify([
engine flutterTextInputView:inputView
1628 updateEditingClient:0
1629 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1630 return ([state[
@"selectionBase"] intValue]) == 0 &&
1631 ([state[@"selectionExtent"] intValue] == 0);
1635 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1636 [inputView updateEditingState];
1637 OCMVerify([
engine flutterTextInputView:inputView
1638 updateEditingClient:0
1639 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1640 return ([state[
@"selectionBase"] intValue]) == 0 &&
1641 ([state[@"selectionExtent"] intValue] == 0);
1645- (void)testUpdateEditingClientSelectionClamping {
1649 [inputView.text setString:@"SELECTION"];
1650 inputView.markedTextRange = nil;
1651 inputView.selectedTextRange = nil;
1654 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1655 [inputView updateEditingState];
1656 OCMVerify([
engine flutterTextInputView:inputView
1657 updateEditingClient:0
1658 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1659 return ([state[
@"selectionBase"] intValue]) == 0 &&
1660 ([state[@"selectionExtent"] intValue] == 0);
1664 [inputView setTextInputState:@{
1665 @"text" : @"SELECTION",
1666 @"selectionBase" : @0,
1667 @"selectionExtent" : @9999
1669 [inputView updateEditingState];
1671 OCMVerify([
engine flutterTextInputView:inputView
1672 updateEditingClient:0
1673 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1674 return ([state[
@"selectionBase"] intValue]) == 0 &&
1675 ([state[@"selectionExtent"] intValue] == 9);
1680 setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1681 [inputView updateEditingState];
1682 OCMVerify([
engine flutterTextInputView:inputView
1683 updateEditingClient:0
1684 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1685 return ([state[
@"selectionBase"] intValue]) == 0 &&
1686 ([state[@"selectionExtent"] intValue] == 1);
1690 [inputView setTextInputState:@{
1691 @"text" : @"SELECTION",
1692 @"selectionBase" : @9999,
1693 @"selectionExtent" : @9999
1695 [inputView updateEditingState];
1696 OCMVerify([
engine flutterTextInputView:inputView
1697 updateEditingClient:0
1698 withState:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
1699 return ([state[
@"selectionBase"] intValue]) == 9 &&
1700 ([state[@"selectionExtent"] intValue] == 9);
1704- (void)testInputViewsHasNonNilInputDelegate {
1706 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1708 [inputView setTextInputClient:123];
1709 [inputView reloadInputViews];
1710 [inputView becomeFirstResponder];
1711 NSAssert(inputView.isFirstResponder,
@"inputView is not first responder");
1712 inputView.inputDelegate = nil;
1716 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1717 OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1718 [inputView removeFromSuperview];
1721- (void)testInputViewsDoNotHaveUITextInteractions {
1723 BOOL hasTextInteraction = NO;
1724 for (
id interaction in inputView.interactions) {
1725 hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1726 if (hasTextInteraction) {
1730 XCTAssertFalse(hasTextInteraction);
1733#pragma mark - UITextInput methods - Tests
1735- (void)testUpdateFirstRectForRange {
1736 [
self setClientId:123 configuration:self.mutableTemplateCopy];
1742 setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1747 NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1748 NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1752 NSArray* affineMatrix = @[
1753 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1754 @(-6.0), @(3.0), @(9.0), @(1.0)
1758 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1760 [inputView setEditableTransform:yOffsetMatrix];
1762 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1765 CGRect testRect = CGRectMake(0, 0, 100, 100);
1766 [inputView setMarkedRect:testRect];
1768 CGRect finalRect = CGRectOffset(testRect, 0, 200);
1769 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1771 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1774 [inputView setEditableTransform:zeroMatrix];
1776 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1777 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1780 [inputView setEditableTransform:yOffsetMatrix];
1781 [inputView setMarkedRect:testRect];
1782 XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1785 [inputView setMarkedRect:kInvalidFirstRect];
1787 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1788 XCTAssertTrue(CGRectEqualToRect(
kInvalidFirstRect, [inputView firstRectForRange:range]));
1791 [inputView setEditableTransform:affineMatrix];
1792 [inputView setMarkedRect:testRect];
1794 CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1796 NSAssert(inputView.superview,
@"inputView is not in the view hierarchy!");
1797 const CGPoint offset = CGPointMake(113, 119);
1798 CGRect currentFrame = inputView.frame;
1799 currentFrame.origin = offset;
1800 inputView.frame = currentFrame;
1803 XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1804 [inputView firstRectForRange:range]));
1807- (void)testFirstRectForRangeReturnsNoneZeroRectWhenScribbleIsEnabled {
1809 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1814 [inputView setSelectionRects:@[
1823 if (@available(iOS 17, *)) {
1824 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1825 [inputView firstRectForRange:multiRectRange]));
1827 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1828 [inputView firstRectForRange:multiRectRange]));
1832- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1834 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1836 [inputView setSelectionRects:@[
1843 if (@available(iOS 17, *)) {
1844 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1845 [inputView firstRectForRange:singleRectRange]));
1847 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1852 if (@available(iOS 17, *)) {
1853 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1854 [inputView firstRectForRange:multiRectRange]));
1856 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1859 [inputView setTextInputState:@{@"text" : @"COM"}];
1861 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1864- (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1866 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1868 [inputView setSelectionRects:@[
1875 if (@available(iOS 17, *)) {
1876 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1877 [inputView firstRectForRange:singleRectRange]));
1879 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1883 if (@available(iOS 17, *)) {
1884 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1885 [inputView firstRectForRange:multiRectRange]));
1887 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1890 [inputView setTextInputState:@{@"text" : @"COM"}];
1892 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1895- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1897 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1899 [inputView setSelectionRects:@[
1910 if (@available(iOS 17, *)) {
1911 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1912 [inputView firstRectForRange:singleRectRange]));
1914 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1919 if (@available(iOS 17, *)) {
1920 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1921 [inputView firstRectForRange:multiRectRange]));
1923 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1927- (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1929 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1931 [inputView setSelectionRects:@[
1942 if (@available(iOS 17, *)) {
1943 XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1944 [inputView firstRectForRange:singleRectRange]));
1946 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:singleRectRange]));
1950 if (@available(iOS 17, *)) {
1951 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1952 [inputView firstRectForRange:multiRectRange]));
1954 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1958- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1960 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1962 [inputView setSelectionRects:@[
1973 if (@available(iOS 17, *)) {
1974 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1975 [inputView firstRectForRange:multiRectRange]));
1977 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
1981- (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1983 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1985 [inputView setSelectionRects:@[
1996 if (@available(iOS 17, *)) {
1997 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1998 [inputView firstRectForRange:multiRectRange]));
2000 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2004- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
2006 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2008 [inputView setSelectionRects:@[
2019 if (@available(iOS 17, *)) {
2020 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
2021 [inputView firstRectForRange:multiRectRange]));
2023 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2027- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
2029 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2031 [inputView setSelectionRects:@[
2042 if (@available(iOS 17, *)) {
2043 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
2044 [inputView firstRectForRange:multiRectRange]));
2046 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2050- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
2052 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2054 [inputView setSelectionRects:@[
2065 if (@available(iOS 17, *)) {
2066 XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
2067 [inputView firstRectForRange:multiRectRange]));
2069 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2073- (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
2075 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2077 [inputView setSelectionRects:@[
2088 if (@available(iOS 17, *)) {
2089 XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
2090 [inputView firstRectForRange:multiRectRange]));
2092 XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:multiRectRange]));
2096- (void)testClosestPositionToPoint {
2098 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2101 [inputView setSelectionRects:@[
2106 CGPoint point = CGPointMake(150, 150);
2107 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2108 XCTAssertEqual(UITextStorageDirectionBackward,
2113 [inputView setSelectionRects:@[
2120 point = CGPointMake(125, 150);
2121 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2122 XCTAssertEqual(UITextStorageDirectionForward,
2127 [inputView setSelectionRects:@[
2134 point = CGPointMake(125, 201);
2135 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2136 XCTAssertEqual(UITextStorageDirectionBackward,
2140 [inputView setSelectionRects:@[
2146 point = CGPointMake(125, 250);
2147 XCTAssertEqual(4U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2148 XCTAssertEqual(UITextStorageDirectionBackward,
2152 [inputView setSelectionRects:@[
2157 point = CGPointMake(110, 50);
2158 XCTAssertEqual(2U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2159 XCTAssertEqual(UITextStorageDirectionForward,
2164 [inputView beginFloatingCursorAtPoint:CGPointZero];
2165 XCTAssertEqual(1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
2166 XCTAssertEqual(UITextStorageDirectionForward,
2168 [inputView endFloatingCursor];
2171- (void)testClosestPositionToPointRTL {
2173 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2175 [inputView setSelectionRects:@[
2191 XCTAssertEqual(0U, position.
index);
2192 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2194 XCTAssertEqual(1U, position.
index);
2195 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2197 XCTAssertEqual(1U, position.
index);
2198 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2200 XCTAssertEqual(2U, position.
index);
2201 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2203 XCTAssertEqual(2U, position.
index);
2204 XCTAssertEqual(UITextStorageDirectionForward, position.
affinity);
2206 XCTAssertEqual(3U, position.
index);
2207 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2209 XCTAssertEqual(3U, position.
index);
2210 XCTAssertEqual(UITextStorageDirectionBackward, position.
affinity);
2213- (void)testSelectionRectsForRange {
2215 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2217 CGRect testRect0 = CGRectMake(100, 100, 100, 100);
2218 CGRect testRect1 = CGRectMake(200, 200, 100, 100);
2219 [inputView setSelectionRects:@[
2228 XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
2229 XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
2230 XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
2234 XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
2235 XCTAssertTrue(CGRectEqualToRect(
2236 CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
2237 [inputView selectionRectsForRange:range][0].rect));
2240- (void)testClosestPositionToPointWithinRange {
2242 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2245 [inputView setSelectionRects:@[
2252 CGPoint point = CGPointMake(125, 150);
2255 3U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2257 UITextStorageDirectionForward,
2258 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2261 [inputView setSelectionRects:@[
2268 point = CGPointMake(125, 150);
2271 1U, ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
2273 UITextStorageDirectionForward,
2274 ((
FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
2277- (void)testClosestPositionToPointWithPartialSelectionRects {
2279 [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
2286 XCTAssertTrue(CGRectEqualToRect(
2289 affinity:UITextStorageDirectionForward]],
2290 CGRectMake(100, 0, 0, 100)));
2293 XCTAssertTrue(CGRectEqualToRect(
2296 affinity:UITextStorageDirectionForward]],
2300#pragma mark - Floating Cursor - Tests
2302- (void)testFloatingCursorDoesNotThrow {
2305 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2306 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2307 [inputView endFloatingCursor];
2308 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2309 [inputView endFloatingCursor];
2312- (void)testFloatingCursor {
2314 [inputView setTextInputState:@{
2316 @"selectionBase" : @1,
2317 @"selectionExtent" : @1,
2328 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2331 XCTAssertTrue(CGRectEqualToRect(
2334 affinity:UITextStorageDirectionForward]],
2335 CGRectMake(0, 0, 0, 100)));
2338 XCTAssertTrue(CGRectEqualToRect(
2341 affinity:UITextStorageDirectionForward]],
2342 CGRectMake(100, 100, 0, 100)));
2343 XCTAssertTrue(CGRectEqualToRect(
2346 affinity:UITextStorageDirectionForward]],
2347 CGRectMake(200, 200, 0, 100)));
2348 XCTAssertTrue(CGRectEqualToRect(
2351 affinity:UITextStorageDirectionForward]],
2352 CGRectMake(300, 300, 0, 100)));
2355 XCTAssertTrue(CGRectEqualToRect(
2358 affinity:UITextStorageDirectionForward]],
2359 CGRectMake(400, 300, 0, 100)));
2361 XCTAssertTrue(CGRectEqualToRect(
2364 affinity:UITextStorageDirectionForward]],
2368 [inputView setTextInputState:@{
2370 @"selectionBase" : @2,
2371 @"selectionExtent" : @2,
2374 XCTAssertTrue(CGRectEqualToRect(
2377 affinity:UITextStorageDirectionBackward]],
2378 CGRectMake(0, 0, 0, 100)));
2381 XCTAssertTrue(CGRectEqualToRect(
2384 affinity:UITextStorageDirectionBackward]],
2385 CGRectMake(100, 0, 0, 100)));
2386 XCTAssertTrue(CGRectEqualToRect(
2389 affinity:UITextStorageDirectionBackward]],
2390 CGRectMake(200, 100, 0, 100)));
2391 XCTAssertTrue(CGRectEqualToRect(
2394 affinity:UITextStorageDirectionBackward]],
2395 CGRectMake(300, 200, 0, 100)));
2396 XCTAssertTrue(CGRectEqualToRect(
2399 affinity:UITextStorageDirectionBackward]],
2400 CGRectMake(400, 300, 0, 100)));
2402 XCTAssertTrue(CGRectEqualToRect(
2405 affinity:UITextStorageDirectionBackward]],
2410 CGRect initialBounds = inputView.bounds;
2411 [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2412 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2413 OCMVerify([
engine flutterTextInputView:inputView
2414 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2416 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2417 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2418 ([state[
@"Y"] isEqualToNumber:@(0)]);
2421 [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2422 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2423 OCMVerify([
engine flutterTextInputView:inputView
2424 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2426 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2427 return ([state[
@"X"] isEqualToNumber:@(333)]) &&
2428 ([state[
@"Y"] isEqualToNumber:@(333)]);
2431 [inputView endFloatingCursor];
2432 XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2433 OCMVerify([
engine flutterTextInputView:inputView
2434 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2436 withPosition:[OCMArg checkWithBlock:^
BOOL(NSDictionary* state) {
2437 return ([state[
@"X"] isEqualToNumber:@(0)]) &&
2438 ([state[
@"Y"] isEqualToNumber:@(0)]);
2442#pragma mark - UIKeyInput Overrides - Tests
2444- (void)testInsertTextAddsPlaceholderSelectionRects {
2447 setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2457 [inputView setSelectionRects:@[ first, second, third, fourth ]];
2460 [inputView insertText:@"in"];
2488#pragma mark - Autofill - Utilities
2490- (NSMutableDictionary*)mutablePasswordTemplateCopy {
2493 @"inputType" : @{
@"name" :
@"TextInuptType.text"},
2494 @"keyboardAppearance" :
@"Brightness.light",
2495 @"obscureText" : @YES,
2496 @"inputAction" :
@"TextInputAction.unspecified",
2497 @"smartDashesType" :
@"0",
2498 @"smartQuotesType" :
@"0",
2499 @"autocorrect" : @YES
2503 return [_passwordTemplate mutableCopy];
2507 return [
self.installedInputViews
2508 filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2511- (void)commitAutofillContextAndVerify {
2515 [textInputPlugin handleMethodCall:methodCall
2516 result:^(id _Nullable result){
2519 XCTAssertEqual(
self.viewsVisibleToAutofill.count,
2524 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2528#pragma mark - Autofill - Tests
2530- (void)testDisablingAutofillOnInputClient {
2531 NSDictionary* config =
self.mutableTemplateCopy;
2532 [config setValue:@"YES" forKey:@"obscureText"];
2534 [
self setClientId:123 configuration:config];
2537 XCTAssertEqualObjects(inputView.textContentType,
@"");
2540- (void)testAutofillEnabledByDefault {
2541 NSDictionary* config =
self.mutableTemplateCopy;
2542 [config setValue:@"NO" forKey:@"obscureText"];
2543 [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2544 forKey:@"autofill"];
2546 [
self setClientId:123 configuration:config];
2549 XCTAssertNil(inputView.textContentType);
2552- (void)testAutofillContext {
2553 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2556 @"uniqueIdentifier" : @"field1",
2557 @"hints" : @[ @"hint1" ],
2558 @"editingValue" : @{@"text" : @""}
2560 forKey:@"autofill"];
2562 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2564 @"uniqueIdentifier" : @"field2",
2565 @"hints" : @[ @"hint2" ],
2566 @"editingValue" : @{@"text" : @""}
2568 forKey:@"autofill"];
2570 NSMutableDictionary* config = [field1 mutableCopy];
2571 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2573 [
self setClientId:123 configuration:config];
2574 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2578 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2579 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2581 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2584 NSMutableDictionary* field3 =
self.mutablePasswordTemplateCopy;
2586 @"uniqueIdentifier" : @"field3",
2587 @"hints" : @[ @"hint3" ],
2588 @"editingValue" : @{@"text" : @""}
2590 forKey:@"autofill"];
2594 [config setValue:@[ field1, field3 ] forKey:@"fields"];
2596 [
self setClientId:123 configuration:config];
2598 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2601 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2602 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2604 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2607 for (NSString*
key in oldContext.allKeys) {
2612 config =
self.mutablePasswordTemplateCopy;
2615 [
self setClientId:124 configuration:config];
2616 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2618 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2621 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2622 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2625 for (NSString*
key in oldContext.allKeys) {
2630 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2634 [
self setClientId:200 configuration:config];
2637 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2640 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2641 XCTAssertEqual(
self.installedInputViews.count, 4ul);
2644 for (NSString*
key in oldContext.allKeys) {
2648 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2651- (void)testCommitAutofillContext {
2652 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2654 @"uniqueIdentifier" : @"field1",
2655 @"hints" : @[ @"hint1" ],
2656 @"editingValue" : @{@"text" : @""}
2658 forKey:@"autofill"];
2660 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2662 @"uniqueIdentifier" : @"field2",
2663 @"hints" : @[ @"hint2" ],
2664 @"editingValue" : @{@"text" : @""}
2666 forKey:@"autofill"];
2668 NSMutableDictionary* field3 =
self.mutableTemplateCopy;
2670 @"uniqueIdentifier" : @"field3",
2671 @"hints" : @[ @"hint3" ],
2672 @"editingValue" : @{@"text" : @""}
2674 forKey:@"autofill"];
2676 NSMutableDictionary* config = [field1 mutableCopy];
2677 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2679 [
self setClientId:123 configuration:config];
2680 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2682 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2684 [
self commitAutofillContextAndVerify];
2685 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2688 [
self setClientId:123 configuration:config];
2690 [
self setClientId:124 configuration:field3];
2691 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
2693 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2694 XCTAssertEqual(
self.installedInputViews.count, 3ul);
2697 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2699 [
self commitAutofillContextAndVerify];
2700 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2703 [
self setClientId:125 configuration:self.mutableTemplateCopy];
2705 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 0ul);
2709 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2710 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2712 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2714 [
self commitAutofillContextAndVerify];
2715 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2718- (void)testAutofillInputViews {
2719 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2721 @"uniqueIdentifier" : @"field1",
2722 @"hints" : @[ @"hint1" ],
2723 @"editingValue" : @{@"text" : @""}
2725 forKey:@"autofill"];
2727 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2729 @"uniqueIdentifier" : @"field2",
2730 @"hints" : @[ @"hint2" ],
2731 @"editingValue" : @{@"text" : @""}
2733 forKey:@"autofill"];
2735 NSMutableDictionary* config = [field1 mutableCopy];
2736 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2738 [
self setClientId:123 configuration:config];
2739 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2742 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2745 XCTAssertEqual(inputFields.count, 2ul);
2746 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 2ul);
2751 withText:@"Autofilled!"];
2752 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2755 OCMVerify([
engine flutterTextInputView:inactiveView
2756 updateEditingClient:0
2757 withState:[OCMArg isNotNil]
2758 withTag:
@"field2"]);
2761- (void)testAutofillContextPersistsAfterClearClient {
2762 NSMutableDictionary* field1 =
self.mutableTemplateCopy;
2764 @"uniqueIdentifier" : @"field1",
2765 @"hints" : @[ @"username" ],
2766 @"editingValue" : @{@"text" : @""}
2768 forKey:@"autofill"];
2770 NSMutableDictionary* field2 =
self.mutablePasswordTemplateCopy;
2772 @"uniqueIdentifier" : @"field2",
2773 @"hints" : @[ @"password" ],
2774 @"editingValue" : @{@"text" : @""}
2776 forKey:@"autofill"];
2778 NSMutableDictionary* config = [field1 mutableCopy];
2779 [config setValue:@[ field1, field2 ] forKey:@"fields"];
2782 [
self setClientId:123 configuration:config];
2787 [
self setClientClear];
2792 [
self commitAutofillContextAndVerify];
2797- (void)testSetClientResetsPendingAutofillRemoval {
2801 NSMutableDictionary* field =
self.mutablePasswordTemplateCopy;
2803 @"uniqueIdentifier" : @"field1",
2804 @"hints" : @[ @"password" ],
2805 @"editingValue" : @{@"text" : @""}
2807 forKey:@"autofill"];
2808 [field setValue:@[ field ] forKey:@"fields"];
2811 [
self setClientId:123 configuration:field];
2815 [
self setClientClear];
2819 [
self setClientId:456 configuration:self.mutableTemplateCopy];
2824- (void)testPendingInputViewRemovalAfterClearClient {
2828 NSDictionary* config =
self.mutableTemplateCopy;
2831 [
self setClientId:123 configuration:config];
2838 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2841 [
self setClientClear];
2846 [
self setTextInputHide];
2851- (void)testHideBeforeClearClientRemovesViewImmediately {
2855 NSDictionary* config =
self.mutableTemplateCopy;
2857 [
self setClientId:123 configuration:config];
2858 [
self setTextInputShow];
2862 [
self setTextInputHide];
2867 [
self setClientClear];
2872- (void)testSetClientResetsPendingInputViewRemoval {
2876 NSDictionary* config =
self.mutableTemplateCopy;
2879 [
self setClientId:123 configuration:config];
2880 [
self setTextInputShow];
2885 OCMStub([mockActiveView isFirstResponder]).andReturn(YES);
2887 [
self setClientClear];
2891 [
self setClientId:456 configuration:config];
2895- (void)testPasswordAutofillHack {
2896 NSDictionary* config =
self.mutableTemplateCopy;
2897 [config setValue:@"YES" forKey:@"obscureText"];
2898 [
self setClientId:123 configuration:config];
2901 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
2905 XCTAssert([inputView isKindOfClass:[UITextField class]]);
2908 XCTAssertNotEqual([inputView performSelector:@selector(font)], nil);
2911- (void)testClearAutofillContextClearsSelection {
2912 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2913 NSDictionary* editingValue = @{
2914 @"text" :
@"REGULAR_TEXT_FIELD",
2915 @"composingBase" : @0,
2916 @"composingExtent" : @3,
2917 @"selectionBase" : @1,
2918 @"selectionExtent" : @4
2920 [regularField setValue:@{
2921 @"uniqueIdentifier" : @"field2",
2922 @"hints" : @[ @"hint2" ],
2923 @"editingValue" : editingValue,
2925 forKey:@"autofill"];
2926 [regularField addEntriesFromDictionary:editingValue];
2927 [
self setClientId:123 configuration:regularField];
2928 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2929 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2932 XCTAssert([oldInputView.text isEqualToString:
@"REGULAR_TEXT_FIELD"]);
2934 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(1, 3)));
2938 [
self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2939 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2941 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2943 [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2944 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2947 XCTAssert([oldInputView.text isEqualToString:
@""]);
2949 XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(0, 0)));
2952- (void)testGarbageInputViewsAreNotRemovedImmediately {
2954 [
self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2955 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2957 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2960 [
self setClientId:124 configuration:self.mutableTemplateCopy];
2961 [
self ensureOnlyActiveViewCanBecomeFirstResponder];
2963 XCTAssertEqual(
self.installedInputViews.count, 2ul);
2965 [
self commitAutofillContextAndVerify];
2968- (void)testScribbleSetSelectionRects {
2969 NSMutableDictionary* regularField =
self.mutableTemplateCopy;
2970 NSDictionary* editingValue = @{
2971 @"text" :
@"REGULAR_TEXT_FIELD",
2972 @"composingBase" : @0,
2973 @"composingExtent" : @3,
2974 @"selectionBase" : @1,
2975 @"selectionExtent" : @4
2977 [regularField setValue:@{
2978 @"uniqueIdentifier" : @"field1",
2979 @"hints" : @[ @"hint2" ],
2980 @"editingValue" : editingValue,
2982 forKey:@"autofill"];
2983 [regularField addEntriesFromDictionary:editingValue];
2984 [
self setClientId:123 configuration:regularField];
2985 XCTAssertEqual(
self.installedInputViews.count, 1ul);
2986 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 0u);
2988 NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2989 NSArray*
selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2993 [textInputPlugin handleMethodCall:methodCall
2994 result:^(id _Nullable result){
2997 XCTAssertEqual([
textInputPlugin.activeView.selectionRects count], 1u);
3000- (void)testDecommissionedViewAreNotReusedByAutofill {
3002 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
3003 [configuration setValue:@{
3004 @"uniqueIdentifier" : @"field1",
3005 @"hints" : @[ UITextContentTypePassword ],
3006 @"editingValue" : @{@"text" : @""}
3008 forKey:@"autofill"];
3009 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3011 [
self setClientId:123 configuration:configuration];
3013 [
self setTextInputHide];
3016 [
self setClientId:124 configuration:configuration];
3020 XCTAssertNotNil(previousActiveView);
3024- (void)testInitialActiveViewCantAccessTextInputDelegate {
3031- (void)testAutoFillDoesNotTriggerOnShowAndHideKeyboard {
3033 NSMutableDictionary* configuration =
self.mutableTemplateCopy;
3034 [configuration setValue:@{
3035 @"uniqueIdentifier" : @"field1",
3036 @"hints" : @[ UITextContentTypePassword ],
3037 @"editingValue" : @{@"text" : @""}
3039 forKey:@"autofill"];
3040 [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
3041 [
self setClientId:123 configuration:configuration];
3043 [
self setTextInputShow];
3044 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3047 [
self setTextInputHide];
3048 XCTAssertEqual(
self.viewsVisibleToAutofill.count, 1ul);
3050 [
self commitAutofillContextAndVerify];
3053#pragma mark - Accessibility - Tests
3055- (void)testUITextInputAccessibilityNotHiddenWhenKeyboardIsShownAndHidden {
3056 [
self setClientId:123 configuration:self.mutableTemplateCopy];
3059 NSArray<FlutterTextInputView*>* inputFields =
self.installedInputViews;
3062 XCTAssertEqual([inputFields count], 1u);
3065 [
self setTextInputShow];
3067 inputFields =
self.installedInputViews;
3069 XCTAssertEqual([inputFields count], 1u);
3072 [
self setTextInputHide];
3074 inputFields =
self.installedInputViews;
3076 XCTAssertEqual([inputFields count], 1u);
3079 [
self setClientClear];
3081 inputFields =
self.installedInputViews;
3084 XCTAssertEqual([inputFields count], 0u);
3087- (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
3090 UIView* container = [[UIView alloc] init];
3091 UIAccessibilityElement* backing =
3092 [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
3093 inputView.backingTextInputAccessibilityObject = backing;
3096 [inputView accessibilityElementDidBecomeFocused];
3102- (void)testFlutterTokenizerCanParseLines {
3104 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3107 FlutterTextRange* range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3108 XCTAssertEqual(range.
range.location, 0u);
3109 XCTAssertEqual(range.
range.length, 0u);
3111 [inputView insertText:@"how are you\nI am fine, Thank you"];
3113 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:0];
3114 XCTAssertEqual(range.
range.location, 0u);
3115 XCTAssertEqual(range.
range.length, 11u);
3117 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:2];
3118 XCTAssertEqual(range.
range.location, 0u);
3119 XCTAssertEqual(range.
range.length, 11u);
3121 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:11];
3122 XCTAssertEqual(range.
range.location, 0u);
3123 XCTAssertEqual(range.
range.length, 11u);
3125 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:12];
3126 XCTAssertEqual(range.
range.location, 12u);
3127 XCTAssertEqual(range.
range.length, 20u);
3129 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:15];
3130 XCTAssertEqual(range.
range.location, 12u);
3131 XCTAssertEqual(range.
range.length, 20u);
3133 range = [
self getLineRangeFromTokenizer:tokenizer atIndex:32];
3134 XCTAssertEqual(range.
range.location, 12u);
3135 XCTAssertEqual(range.
range.length, 20u);
3138- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
3140 [inputView insertText:@"0123456789\n012345"];
3141 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3144 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3145 withGranularity:UITextGranularityLine
3146 inDirection:UITextStorageDirectionBackward];
3147 XCTAssertEqual(range.
range.location, 11u);
3148 XCTAssertEqual(range.
range.length, 6u);
3151- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
3153 [inputView insertText:@"0123456789\n012345"];
3154 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3157 (
FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
3158 withGranularity:UITextGranularityLine
3159 inDirection:UITextStorageDirectionForward];
3160 if (@available(iOS 17.0, *)) {
3161 XCTAssertNil(range);
3163 XCTAssertEqual(range.
range.location, 11u);
3164 XCTAssertEqual(range.
range.length, 6u);
3168- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
3170 [inputView insertText:@"0123456789\n012345"];
3171 id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
3176 withGranularity:UITextGranularityLine
3177 inDirection:UITextStorageDirectionForward];
3178 if (@available(iOS 17.0, *)) {
3179 XCTAssertNil(range);
3181 XCTAssertEqual(range.
range.location, 0u);
3182 XCTAssertEqual(range.
range.length, 0u);
3186- (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
3191 __weak UIView* activeView;
3196 [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
3199 result:^(id _Nullable result){
3205 result:^(id _Nullable result){
3207 XCTAssertNotNil(activeView);
3210 XCTAssertNotNil(activeView);
3213- (void)testFlutterTextInputPluginHostViewNilCrash {
3216 XCTAssertThrows([myInputPlugin hostView],
@"Throws exception if host view is nil");
3219- (void)testFlutterTextInputPluginHostViewNotNil {
3225 XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
3228- (void)testSetPlatformViewClient {
3235 arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
3237 result:^(id _Nullable result){
3240 XCTAssertNotNil(activeView.superview,
@"activeView must be added to the view hierarchy.");
3243 arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
3245 result:^(id _Nullable result){
3247 XCTAssertNil(activeView.superview,
@"activeView must be removed from view hierarchy.");
3250- (void)testEditMenu_shouldSetupEditMenuDelegateCorrectly {
3251 if (@available(iOS 16.0, *)) {
3253 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3254 XCTAssertEqual(inputView.editMenuInteraction.delegate, inputView,
3255 @"editMenuInteraction setup delegate correctly");
3259- (void)testEditMenu_shouldNotPresentEditMenuIfNotFirstResponder {
3260 if (@available(iOS 16.0, *)) {
3264 XCTAssertFalse(shownEditMenu,
@"Should not show edit menu if not first responder.");
3268- (void)testEditMenu_shouldPresentEditMenuWithCorrectConfiguration {
3269 if (@available(iOS 16.0, *)) {
3274 [myViewController loadView];
3277 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3279 result:^(id _Nullable result){
3285 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3287 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3288 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3290 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3291 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3292 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3293 .andDo(^(NSInvocation* invocation) {
3295 [invocation retainArguments];
3296 UIEditMenuConfiguration* config;
3297 [invocation getArgument:&config atIndex:2];
3298 XCTAssertEqual(config.preferredArrowDirection, UIEditMenuArrowDirectionAutomatic,
3299 @"UIEditMenuConfiguration must use automatic arrow direction.");
3300 XCTAssert(CGPointEqualToPoint(config.sourcePoint, CGPointZero),
3301 @"UIEditMenuConfiguration must have the correct point.");
3302 [expectation fulfill];
3305 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3306 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(0),
@"height" : @(0)};
3308 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3309 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3310 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3314- (void)testEditMenu_shouldPresentEditMenuWithCorectTargetRect {
3315 if (@available(iOS 16.0, *)) {
3320 [myViewController loadView];
3324 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3326 result:^(id _Nullable result){
3332 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3334 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3335 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3337 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3338 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3339 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3340 .andDo(^(NSInvocation* invocation) {
3341 [expectation fulfill];
3344 myInputView.frame = CGRectMake(10, 20, 30, 40);
3345 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3346 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3348 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3349 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3350 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3353 [myInputView editMenuInteraction:mockInteraction
3354 targetRectForConfiguration:OCMClassMock([UIEditMenuConfiguration class])];
3356 XCTAssert(CGRectEqualToRect(targetRect, CGRectMake(90, 180, 300, 400)),
3357 @"targetRectForConfiguration must return the correct target rect.");
3361- (void)testEditMenu_shouldPresentEditMenuWithSuggestedItemsByDefaultIfNoFrameworkData {
3362 if (@available(iOS 16.0, *)) {
3367 [myViewController loadView];
3371 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3373 result:^(id _Nullable result){
3379 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3381 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3382 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3384 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3385 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3386 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3387 .andDo(^(NSInvocation* invocation) {
3388 [expectation fulfill];
3391 myInputView.frame = CGRectMake(10, 20, 30, 40);
3392 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3393 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3395 BOOL shownEditMenu = [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect}];
3396 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3397 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3399 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3401 action:@selector(copy:)
3403 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3405 action:@selector(paste:)
3407 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3409 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3410 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3411 suggestedActions:suggestedActions];
3412 XCTAssertEqualObjects(menu.children, suggestedActions,
3413 @"Must show suggested items by default.");
3417- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsAndCorrectOrderingForBasicEditingActions {
3418 if (@available(iOS 16.0, *)) {
3423 [myViewController loadView];
3427 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3429 result:^(id _Nullable result){
3435 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3437 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3438 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3440 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3441 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3442 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3443 .andDo(^(NSInvocation* invocation) {
3444 [expectation fulfill];
3447 myInputView.frame = CGRectMake(10, 20, 30, 40);
3448 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3449 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3451 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3452 @[ @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3454 BOOL shownEditMenu =
3455 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3456 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3457 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3459 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3461 action:@selector(copy:)
3463 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3465 action:@selector(paste:)
3467 NSArray<UICommand*>* suggestedActions = @[ copyItem, pasteItem ];
3469 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3470 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3471 suggestedActions:suggestedActions];
3473 NSArray<UICommand*>* expectedChildren = @[ pasteItem, copyItem ];
3474 XCTAssertEqualObjects(menu.children, expectedChildren);
3478- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsUnderNestedSubtreeForBasicEditingActions {
3479 if (@available(iOS 16.0, *)) {
3484 [myViewController loadView];
3488 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3490 result:^(id _Nullable result){
3496 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3498 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3499 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3501 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3502 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3503 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3504 .andDo(^(NSInvocation* invocation) {
3505 [expectation fulfill];
3508 myInputView.frame = CGRectMake(10, 20, 30, 40);
3509 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3510 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3512 NSArray<NSDictionary<NSString*, id>*>* encodedItems =
3513 @[ @{@"type" : @"cut"}, @{@"type" : @"paste"}, @{@"type" : @"copy"} ];
3515 BOOL shownEditMenu =
3516 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3517 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3518 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3520 UICommand* copyItem = [UICommand commandWithTitle:@"Copy"
3522 action:@selector(copy:)
3524 UICommand* cutItem = [UICommand commandWithTitle:@"Cut"
3526 action:@selector(cut:)
3528 UICommand* pasteItem = [UICommand commandWithTitle:@"Paste"
3530 action:@selector(paste:)
3543 NSArray<UIMenuElement*>* suggestedActions = @[
3544 copyItem, [UIMenu menuWithChildren:@[ pasteItem ]],
3545 [UIMenu menuWithChildren:@[ [UIMenu menuWithChildren:@[ cutItem ]] ]]
3548 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3549 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3550 suggestedActions:suggestedActions];
3552 NSArray<UICommand*>* expectedActions = @[ cutItem, pasteItem, copyItem ];
3553 XCTAssertEqualObjects(menu.children, expectedActions);
3557- (void)testEditMenu_shouldPresentEditMenuWithCorectItemsForMoreAdditionalItems {
3558 if (@available(iOS 16.0, *)) {
3563 [myViewController loadView];
3567 arguments:@[ @(123),
self.mutableTemplateCopy ]];
3569 result:^(id _Nullable result){
3575 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
3577 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3578 initWithDescription:@"presentEditMenuWithConfiguration must be called."];
3580 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
3581 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
3582 OCMStub([mockInteraction presentEditMenuWithConfiguration:[OCMArg any]])
3583 .andDo(^(NSInvocation* invocation) {
3584 [expectation fulfill];
3587 myInputView.frame = CGRectMake(10, 20, 30, 40);
3588 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
3589 @{
@"x" : @(100),
@"y" : @(200),
@"width" : @(300),
@"height" : @(400)};
3591 NSArray<NSDictionary<NSString*, id>*>* encodedItems = @[
3592 @{@"type" : @"searchWeb", @"title" : @"Search Web"},
3593 @{@"type" : @"lookUp", @"title" : @"Look Up"}, @{@"type" : @"share", @"title" : @"Share"}
3596 BOOL shownEditMenu =
3597 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
3598 XCTAssertTrue(shownEditMenu,
@"Should show edit menu with correct configuration.");
3599 [
self waitForExpectations:@[ expectation ] timeout:1.0];
3601 NSArray<UICommand*>* suggestedActions = @[
3602 [UICommand commandWithTitle:@"copy" image:nil action:@selector(copy:) propertyList:nil],
3605 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
3606 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
3607 suggestedActions:suggestedActions];
3608 XCTAssert(menu.children.count == 3,
@"There must be 3 menu items");
3610 XCTAssert(((UICommand*)menu.children[0]).action ==
@selector(handleSearchWebAction),
3611 @"Must create search web item in the tree.");
3612 XCTAssert(((UICommand*)menu.children[1]).action ==
@selector(handleLookUpAction),
3613 @"Must create look up item in the tree.");
3614 XCTAssert(((UICommand*)menu.children[2]).action ==
@selector(handleShareAction),
3615 @"Must create share item in the tree.");
3619- (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
3620 if (@available(iOS 17.0, *)) {
3621 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3622 "https://github.com/flutter/flutter/issues/183473");
3625 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3627 [inputView setTextInputClient:123];
3628 [inputView reloadInputViews];
3629 [inputView becomeFirstResponder];
3630 XCTAssert(inputView.isFirstResponder);
3632 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3633 [NSNotificationCenter.defaultCenter
3634 postNotificationName:UIKeyboardWillShowNotification
3636 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3640 [textInputPlugin handleMethodCall:onPointerMoveCall
3641 result:^(id _Nullable result){
3643 XCTAssertFalse(inputView.isFirstResponder);
3647- (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
3648 if (@available(iOS 17.0, *)) {
3649 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3650 "https://github.com/flutter/flutter/issues/183473");
3652 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3653 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3654 UIScene* scene = scenes.anyObject;
3655 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3656 UIWindowScene* windowScene = (UIWindowScene*)scene;
3657 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3658 UIWindow*
window = windowScene.windows[0];
3659 [window addSubview:viewController.view];
3661 [viewController loadView];
3664 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3666 [inputView setTextInputClient:123];
3667 [inputView reloadInputViews];
3668 [inputView becomeFirstResponder];
3671 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3672 [subView removeFromSuperview];
3676 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3677 [NSNotificationCenter.defaultCenter
3678 postNotificationName:UIKeyboardWillShowNotification
3680 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3684 [textInputPlugin handleMethodCall:onPointerMoveCall
3685 result:^(id _Nullable result){
3688 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3689 [subView removeFromSuperview];
3694- (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
3695 if (@available(iOS 17.0, *)) {
3696 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3697 "https://github.com/flutter/flutter/issues/183473");
3699 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3700 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3701 UIScene* scene = scenes.anyObject;
3702 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3703 UIWindowScene* windowScene = (UIWindowScene*)scene;
3704 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3705 UIWindow*
window = windowScene.windows[0];
3706 [window addSubview:viewController.view];
3708 [viewController loadView];
3711 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3713 [inputView setTextInputClient:123];
3714 [inputView reloadInputViews];
3715 [inputView becomeFirstResponder];
3717 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3718 [NSNotificationCenter.defaultCenter
3719 postNotificationName:UIKeyboardWillShowNotification
3721 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3725 [textInputPlugin handleMethodCall:onPointerMoveCall
3726 result:^(id _Nullable result){
3730 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3735 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3736 result:^(id _Nullable result){
3740 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3742 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3743 [subView removeFromSuperview];
3748- (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
3749 if (@available(iOS 17.0, *)) {
3750 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3751 "https://github.com/flutter/flutter/issues/183473");
3753 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3754 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3755 UIScene* scene = scenes.anyObject;
3756 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3757 UIWindowScene* windowScene = (UIWindowScene*)scene;
3758 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3759 UIWindow*
window = windowScene.windows[0];
3760 [window addSubview:viewController.view];
3762 [viewController loadView];
3765 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3767 [inputView setTextInputClient:123];
3768 [inputView reloadInputViews];
3769 [inputView becomeFirstResponder];
3771 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3772 [NSNotificationCenter.defaultCenter
3773 postNotificationName:UIKeyboardWillShowNotification
3775 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3779 [textInputPlugin handleMethodCall:onPointerMoveCall
3780 result:^(id _Nullable result){
3783 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3788 [textInputPlugin handleMethodCall:onPointerMoveCallMove
3789 result:^(id _Nullable result){
3792 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
3797 [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
3798 result:^(id _Nullable result){
3801 XCTAssertEqual(
textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
3802 for (UIView* subView in
textInputPlugin.keyboardViewContainer.subviews) {
3803 [subView removeFromSuperview];
3808- (void)testInteractiveKeyboardFindFirstResponderRecursive {
3810 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3811 [inputView setTextInputClient:123];
3812 [inputView reloadInputViews];
3813 [inputView becomeFirstResponder];
3815 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3816 XCTAssertEqualObjects(inputView, firstResponder);
3820- (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
3827 [subInputView addSubview:subFirstResponderInputView];
3828 [inputView addSubview:subInputView];
3829 [inputView addSubview:otherSubInputView];
3830 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3831 [inputView setTextInputClient:123];
3832 [inputView reloadInputViews];
3833 [subInputView setTextInputClient:123];
3834 [subInputView reloadInputViews];
3835 [otherSubInputView setTextInputClient:123];
3836 [otherSubInputView reloadInputViews];
3837 [subFirstResponderInputView setTextInputClient:123];
3838 [subFirstResponderInputView reloadInputViews];
3839 [subFirstResponderInputView becomeFirstResponder];
3841 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3842 XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
3846- (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
3848 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3849 [inputView setTextInputClient:123];
3850 [inputView reloadInputViews];
3852 UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
3853 XCTAssertNil(firstResponder);
3857- (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
3858 if (@available(iOS 17.0, *)) {
3859 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3860 "https://github.com/flutter/flutter/issues/183473");
3862 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3863 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3864 UIScene* scene = scenes.anyObject;
3865 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3866 UIWindowScene* windowScene = (UIWindowScene*)scene;
3867 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3868 UIWindow*
window = windowScene.windows[0];
3869 [window addSubview:viewController.view];
3871 [viewController loadView];
3873 XCTestExpectation* expectation = [[XCTestExpectation alloc]
3874 initWithDescription:
3875 @"didResignFirstResponder is called after screenshot keyboard dismissed."];
3876 OCMStub([
engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
3877 .andDo(^(NSInvocation* invocation) {
3878 [expectation fulfill];
3880 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3881 [NSNotificationCenter.defaultCenter
3882 postNotificationName:UIKeyboardWillShowNotification
3884 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3888 [textInputPlugin handleMethodCall:initialMoveCall
3889 result:^(id _Nullable result){
3894 [textInputPlugin handleMethodCall:subsequentMoveCall
3895 result:^(id _Nullable result){
3901 [textInputPlugin handleMethodCall:pointerUpCall
3902 result:^(id _Nullable result){
3905 [
self waitForExpectations:@[ expectation ] timeout:2.0];
3909- (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
3910 if (@available(iOS 17.0, *)) {
3911 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3912 "https://github.com/flutter/flutter/issues/183473");
3914 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3915 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3916 UIScene* scene = scenes.anyObject;
3917 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3918 UIWindowScene* windowScene = (UIWindowScene*)scene;
3919 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3920 UIWindow*
window = windowScene.windows[0];
3921 [window addSubview:viewController.view];
3923 [viewController loadView];
3925 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3926 [NSNotificationCenter.defaultCenter
3927 postNotificationName:UIKeyboardWillShowNotification
3929 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3933 [textInputPlugin handleMethodCall:initialMoveCall
3934 result:^(id _Nullable result){
3939 [textInputPlugin handleMethodCall:subsequentMoveCall
3940 result:^(id _Nullable result){
3946 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3947 result:^(id _Nullable result){
3953 [textInputPlugin handleMethodCall:pointerUpCall
3954 result:^(id _Nullable result){
3956 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3957 return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3959 XCTNSPredicateExpectation* expectation =
3960 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3961 [
self waitForExpectations:@[ expectation ] timeout:10.0];
3965- (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3966 if (@available(iOS 17.0, *)) {
3967 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
3968 "https://github.com/flutter/flutter/issues/183473");
3970 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3971 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
3972 UIScene* scene = scenes.anyObject;
3973 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
3974 UIWindowScene* windowScene = (UIWindowScene*)scene;
3975 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
3976 UIWindow*
window = windowScene.windows[0];
3977 [window addSubview:viewController.view];
3979 [viewController loadView];
3982 [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3984 [inputView setTextInputClient:123];
3985 [inputView reloadInputViews];
3986 [inputView becomeFirstResponder];
3988 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3989 [NSNotificationCenter.defaultCenter
3990 postNotificationName:UIKeyboardWillShowNotification
3992 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3996 [textInputPlugin handleMethodCall:initialMoveCall
3997 result:^(id _Nullable result){
4002 [textInputPlugin handleMethodCall:subsequentMoveCall
4003 result:^(id _Nullable result){
4009 [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
4010 result:^(id _Nullable result){
4016 [textInputPlugin handleMethodCall:pointerUpCall
4017 result:^(id _Nullable result){
4019 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4020 return textInputPlugin.cachedFirstResponder.isFirstResponder;
4022 XCTNSPredicateExpectation* expectation =
4023 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4024 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4028- (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
4029 if (@available(iOS 17.0, *)) {
4030 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4031 "https://github.com/flutter/flutter/issues/183473");
4033 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4034 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4035 UIScene* scene = scenes.anyObject;
4036 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
4037 UIWindowScene* windowScene = (UIWindowScene*)scene;
4038 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4039 UIWindow*
window = windowScene.windows[0];
4040 [window addSubview:viewController.view];
4042 [viewController loadView];
4044 XCTestExpectation* expectation =
4045 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4046 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4047 [NSNotificationCenter.defaultCenter
4048 postNotificationName:UIKeyboardWillShowNotification
4050 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4054 [textInputPlugin handleMethodCall:initialMoveCall
4055 result:^(id _Nullable result){
4060 [textInputPlugin handleMethodCall:subsequentMoveCall
4061 result:^(id _Nullable result){
4066 [textInputPlugin handleMethodCall:upwardVelocityMoveCall
4067 result:^(id _Nullable result){
4074 handleMethodCall:pointerUpCall
4075 result:^(id _Nullable result) {
4076 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4077 viewController.flutterScreenIfViewLoaded.bounds.size.height -
4078 keyboardFrame.origin.y);
4079 [expectation fulfill];
4084- (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
4085 if (@available(iOS 17.0, *)) {
4086 XCTSkip(
@"Interactive keyboard tests broken on iOS 17+ due to SDK bugs. See: "
4087 "https://github.com/flutter/flutter/issues/183473");
4089 NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
4090 XCTAssertEqual(scenes.count, 1UL,
@"There must only be 1 scene for test");
4091 UIScene* scene = scenes.anyObject;
4092 XCTAssert([scene isKindOfClass:[UIWindowScene class]],
@"Must be a window scene for test");
4093 UIWindowScene* windowScene = (UIWindowScene*)scene;
4094 XCTAssert(windowScene.windows.count > 0,
@"There must be at least 1 window for test");
4095 UIWindow*
window = windowScene.windows[0];
4096 [window addSubview:viewController.view];
4098 [viewController loadView];
4100 XCTestExpectation* expectation =
4101 [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
4102 CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
4103 [NSNotificationCenter.defaultCenter
4104 postNotificationName:UIKeyboardWillShowNotification
4106 userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
4110 [textInputPlugin handleMethodCall:initialMoveCall
4111 result:^(id _Nullable result){
4116 [textInputPlugin handleMethodCall:subsequentMoveCall
4117 result:^(id _Nullable result){
4124 handleMethodCall:pointerUpCall
4125 result:^(id _Nullable result) {
4126 XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
4127 viewController.flutterScreenIfViewLoaded.bounds.size.height);
4128 [expectation fulfill];
4132- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
4133 [UIView setAnimationsEnabled:YES];
4134 [textInputPlugin showKeyboardAndRemoveScreenshot];
4136 UIView.areAnimationsEnabled,
4137 @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
4140- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
4141 [UIView setAnimationsEnabled:YES];
4142 [textInputPlugin showKeyboardAndRemoveScreenshot];
4144 NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
4146 return UIView.areAnimationsEnabled;
4148 XCTNSPredicateExpectation* expectation =
4149 [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
4150 [
self waitForExpectations:@[ expectation ] timeout:10.0];
4153- (void)testEditMenu_shouldCreateCustomMenuItemWithCorrectProperties {
4154 if (@available(iOS 16.0, *)) {
4159 [myViewController loadView];
4163 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4165 result:^(id _Nullable result){
4170 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4172 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4173 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4175 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4176 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4178 NSArray<NSDictionary*>* encodedItems = @[
4179 @{@"type" : @"custom", @"id" : @"custom-action-1", @"title" : @"Custom Action 1"},
4180 @{@"type" : @"custom", @"id" : @"custom-action-2", @"title" : @"Custom Action 2"},
4183 BOOL shownEditMenu =
4184 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4185 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4187 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4188 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4189 suggestedActions:@[]];
4191 XCTAssertEqual(menu.children.count, 2UL,
@"Should create 2 custom menu items");
4192 UIAction* firstAction = (UIAction*)menu.children[0];
4193 UIAction* secondAction = (UIAction*)menu.children[1];
4194 XCTAssertEqualObjects(firstAction.title,
@"Custom Action 1",
4195 @"First action title should match");
4196 XCTAssertEqualObjects(secondAction.title,
@"Custom Action 2",
4197 @"Second action title should match");
4201- (void)testEditMenu_customActionShouldTriggerDelegateCallback {
4202 if (@available(iOS 16.0, *)) {
4205 OCMStub([mockEngine platformChannel]).andReturn(mockPlatformChannel);
4207 OCMStub([mockEngine flutterTextInputView:[OCMArg any]
4208 performContextMenuCustomActionWithActionID:
@"test-callback-id"
4209 textInputClient:123])
4210 .andDo((^(NSInvocation* invocation) {
4211 [mockPlatformChannel invokeMethod:@"ContextMenu.onPerformCustomAction"
4212 arguments:@[ @(123), @"test-callback-id" ]];
4219 [myViewController loadView];
4223 arguments:@[ @(123),
self.mutableTemplateCopy ]];
4225 result:^(id _Nullable result){
4230 OCMStub([mockInputView isFirstResponder]).andReturn(YES);
4231 XCTestExpectation* expectation = [[XCTestExpectation alloc]
4232 initWithDescription:@"Custom action delegate callback should be called"];
4233 OCMStub(([mockPlatformChannel invokeMethod:
@"ContextMenu.onPerformCustomAction"
4234 arguments:@[ @(123),
@"test-callback-id" ]]))
4235 .andDo(^(NSInvocation* invocation) {
4236 [expectation fulfill];
4238 id mockInteraction = OCMClassMock([UIEditMenuInteraction
class]);
4239 OCMStub([mockInputView editMenuInteraction]).andReturn(mockInteraction);
4241 NSDictionary<NSString*, NSNumber*>* encodedTargetRect =
4242 @{
@"x" : @(0),
@"y" : @(0),
@"width" : @(100),
@"height" : @(50)};
4244 NSArray<NSDictionary*>* encodedItems = @[
4245 @{@"type" : @"custom", @"id" : @"test-callback-id", @"title" : @"Test Action"},
4248 BOOL shownEditMenu =
4249 [myInputPlugin
showEditMenu:@{@"targetRect" : encodedTargetRect, @"items" : encodedItems}];
4250 XCTAssertTrue(shownEditMenu,
@"Should show edit menu");
4252 UIMenu* menu = [myInputView editMenuInteraction:mockInteraction
4253 menuForConfiguration:OCMClassMock([UIEditMenuConfiguration class])
4254 suggestedActions:@[]];
4256 XCTAssertEqual(menu.children.count, 1UL,
@"Should have 1 custom menu item");
4257 UIAction* customAction = (UIAction*)menu.children[0];
4258 XCTAssertEqualObjects(customAction.title,
@"Test Action",
@"Action title should match");
4260 [myInputView.textInputDelegate flutterTextInputView:myInputView
4261 performContextMenuCustomActionWithActionID:@"test-callback-id"
4262 textInputClient:123];
4264 [
self waitForExpectations:@[ expectation ] timeout:1.0];
4265 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