Flutter iOS Embedder
FlutterTextInputPluginTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
8 
9 #import <OCMock/OCMock.h>
10 #import <XCTest/XCTest.h>
11 
16 
18 
19 @interface FlutterEngine ()
21 @end
22 
23 @interface FlutterTextInputView ()
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 @end
34 
36 @property(nonatomic, assign) UIAccessibilityNotifications receivedNotification;
37 @property(nonatomic, assign) id receivedNotificationTarget;
38 @property(nonatomic, assign) BOOL isAccessibilityFocused;
39 
40 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target;
41 
42 @end
43 
44 @implementation FlutterTextInputViewSpy {
45 }
46 
47 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
48  self.receivedNotification = notification;
49  self.receivedNotificationTarget = target;
50 }
51 
52 - (BOOL)accessibilityElementIsFocused {
53  return _isAccessibilityFocused;
54 }
55 
56 @end
57 
59 @property(nonatomic, strong) UITextField* textField;
60 @end
61 
62 @interface FlutterTextInputPlugin ()
63 @property(nonatomic, assign) FlutterTextInputView* activeView;
64 @property(nonatomic, readonly) UIView* inputHider;
65 @property(nonatomic, readonly) UIView* keyboardViewContainer;
66 @property(nonatomic, readonly) UIView* keyboardView;
67 @property(nonatomic, assign) UIView* cachedFirstResponder;
68 @property(nonatomic, readonly) CGRect keyboardRect;
69 @property(nonatomic, readonly)
70  NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
71 
72 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
73  clearText:(BOOL)clearText
74  delayRemoval:(BOOL)delayRemoval;
75 - (NSArray<UIView*>*)textInputViews;
76 - (UIView*)hostView;
77 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView;
78 - (void)startLiveTextInput;
79 - (void)showKeyboardAndRemoveScreenshot;
80 
81 @end
82 
83 @interface FlutterTextInputPluginTest : XCTestCase
84 @end
85 
86 @implementation FlutterTextInputPluginTest {
87  NSDictionary* _template;
88  NSDictionary* _passwordTemplate;
89  id engine;
91 
93 }
94 
95 - (void)setUp {
96  [super setUp];
97  engine = OCMClassMock([FlutterEngine class]);
98 
99  textInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
100 
101  viewController = [[FlutterViewController alloc] init];
103 
104  // Clear pasteboard between tests.
105  UIPasteboard.generalPasteboard.items = @[];
106 }
107 
108 - (void)tearDown {
109  textInputPlugin = nil;
110  engine = nil;
111  [textInputPlugin.autofillContext removeAllObjects];
112  [textInputPlugin cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
113  [[[[textInputPlugin textInputView] superview] subviews]
114  makeObjectsPerformSelector:@selector(removeFromSuperview)];
115  viewController = nil;
116  [super tearDown];
117 }
118 
119 - (void)setClientId:(int)clientId configuration:(NSDictionary*)config {
120  FlutterMethodCall* setClientCall =
121  [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
122  arguments:@[ [NSNumber numberWithInt:clientId], config ]];
123  [textInputPlugin handleMethodCall:setClientCall
124  result:^(id _Nullable result){
125  }];
126 }
127 
128 - (void)setTextInputShow {
129  FlutterMethodCall* setClientCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
130  arguments:@[]];
131  [textInputPlugin handleMethodCall:setClientCall
132  result:^(id _Nullable result){
133  }];
134 }
135 
136 - (void)setTextInputHide {
137  FlutterMethodCall* setClientCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.hide"
138  arguments:@[]];
139  [textInputPlugin handleMethodCall:setClientCall
140  result:^(id _Nullable result){
141  }];
142 }
143 
144 - (void)flushScheduledAsyncBlocks {
145  __block bool done = false;
146  XCTestExpectation* expectation =
147  [[XCTestExpectation alloc] initWithDescription:@"Testing on main queue"];
148  dispatch_async(dispatch_get_main_queue(), ^{
149  done = true;
150  });
151  dispatch_async(dispatch_get_main_queue(), ^{
152  XCTAssertTrue(done);
153  [expectation fulfill];
154  });
155  [self waitForExpectations:@[ expectation ] timeout:10];
156 }
157 
158 - (NSMutableDictionary*)mutableTemplateCopy {
159  if (!_template) {
160  _template = @{
161  @"inputType" : @{@"name" : @"TextInuptType.text"},
162  @"keyboardAppearance" : @"Brightness.light",
163  @"obscureText" : @NO,
164  @"inputAction" : @"TextInputAction.unspecified",
165  @"smartDashesType" : @"0",
166  @"smartQuotesType" : @"0",
167  @"autocorrect" : @YES,
168  @"enableInteractiveSelection" : @YES,
169  };
170  }
171 
172  return [_template mutableCopy];
173 }
174 
175 - (NSArray<FlutterTextInputView*>*)installedInputViews {
176  return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
177  filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
178  [FlutterTextInputView class]]];
179 }
180 
181 - (FlutterTextRange*)getLineRangeFromTokenizer:(id<UITextInputTokenizer>)tokenizer
182  atIndex:(NSInteger)index {
183  UITextRange* range =
184  [tokenizer rangeEnclosingPosition:[FlutterTextPosition positionWithIndex:index]
185  withGranularity:UITextGranularityLine
186  inDirection:UITextLayoutDirectionRight];
187  XCTAssertTrue([range isKindOfClass:[FlutterTextRange class]]);
188  return (FlutterTextRange*)range;
189 }
190 
191 - (void)updateConfig:(NSDictionary*)config {
192  FlutterMethodCall* updateConfigCall =
193  [FlutterMethodCall methodCallWithMethodName:@"TextInput.updateConfig" arguments:config];
194  [textInputPlugin handleMethodCall:updateConfigCall
195  result:^(id _Nullable result){
196  }];
197 }
198 
199 #pragma mark - Tests
200 
201 - (void)testWillNotCrashWhenViewControllerIsNil {
202  FlutterEngine* flutterEngine = [[FlutterEngine alloc] init];
203  FlutterTextInputPlugin* inputPlugin =
204  [[FlutterTextInputPlugin alloc] initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
205  XCTAssertNil(inputPlugin.viewController);
206  FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
207  arguments:nil];
208  XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription:@"result called"];
209 
210  [inputPlugin handleMethodCall:methodCall
211  result:^(id _Nullable result) {
212  XCTAssertNil(result);
213  [expectation fulfill];
214  }];
215  XCTAssertNil(inputPlugin.activeView);
216  [self waitForExpectations:@[ expectation ] timeout:1.0];
217 }
218 
219 - (void)testInvokeStartLiveTextInput {
220  FlutterMethodCall* methodCall =
221  [FlutterMethodCall methodCallWithMethodName:@"TextInput.startLiveTextInput" arguments:nil];
222  FlutterTextInputPlugin* mockPlugin = OCMPartialMock(textInputPlugin);
223  [mockPlugin handleMethodCall:methodCall
224  result:^(id _Nullable result){
225  }];
226  OCMVerify([mockPlugin startLiveTextInput]);
227 }
228 
229 - (void)testNoDanglingEnginePointer {
230  __weak FlutterTextInputPlugin* weakFlutterTextInputPlugin;
231  FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
232  __weak FlutterEngine* weakFlutterEngine;
233 
234  FlutterTextInputView* currentView;
235 
236  // The engine instance will be deallocated after the autorelease pool is drained.
237  @autoreleasepool {
238  FlutterEngine* flutterEngine = OCMClassMock([FlutterEngine class]);
239  weakFlutterEngine = flutterEngine;
240  NSAssert(weakFlutterEngine, @"flutter engine must not be nil");
241  FlutterTextInputPlugin* flutterTextInputPlugin = [[FlutterTextInputPlugin alloc]
242  initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
243  weakFlutterTextInputPlugin = flutterTextInputPlugin;
244  flutterTextInputPlugin.viewController = flutterViewController;
245 
246  // Set client so the text input plugin has an active view.
247  NSDictionary* config = self.mutableTemplateCopy;
248  FlutterMethodCall* setClientCall =
249  [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
250  arguments:@[ [NSNumber numberWithInt:123], config ]];
251  [flutterTextInputPlugin handleMethodCall:setClientCall
252  result:^(id _Nullable result){
253  }];
254  currentView = flutterTextInputPlugin.activeView;
255  }
256 
257  NSAssert(!weakFlutterEngine, @"flutter engine must be nil");
258  NSAssert(currentView, @"current view must not be nil");
259 
260  XCTAssertNil(weakFlutterTextInputPlugin);
261  // Verify that the view can no longer access the deallocated engine/text input plugin
262  // instance.
263  XCTAssertNil(currentView.textInputDelegate);
264 }
265 
266 - (void)testSecureInput {
267  NSDictionary* config = self.mutableTemplateCopy;
268  [config setValue:@"YES" forKey:@"obscureText"];
269  [self setClientId:123 configuration:config];
270 
271  // Find all the FlutterTextInputViews we created.
272  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
273 
274  // There are no autofill and the mock framework requested a secure entry. The first and only
275  // inserted FlutterTextInputView should be a secure text entry one.
276  FlutterTextInputView* inputView = inputFields[0];
277 
278  // Verify secureTextEntry is set to the correct value.
279  XCTAssertTrue(inputView.secureTextEntry);
280 
281  // Verify keyboardType is set to the default value.
282  XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeDefault);
283 
284  // We should have only ever created one FlutterTextInputView.
285  XCTAssertEqual(inputFields.count, 1ul);
286 
287  // The one FlutterTextInputView we inserted into the view hierarchy should be the text input
288  // plugin's active text input view.
289  XCTAssertEqual(inputView, textInputPlugin.textInputView);
290 
291  // Despite not given an id in configuration, inputView has
292  // an autofill id.
293  XCTAssert(inputView.autofillId.length > 0);
294 }
295 
296 - (void)testKeyboardType {
297  NSDictionary* config = self.mutableTemplateCopy;
298  [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
299  [self setClientId:123 configuration:config];
300 
301  // Find all the FlutterTextInputViews we created.
302  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
303 
304  FlutterTextInputView* inputView = inputFields[0];
305 
306  // Verify keyboardType is set to the value specified in config.
307  XCTAssertEqual(inputView.keyboardType, UIKeyboardTypeURL);
308 }
309 
310 - (void)testSettingKeyboardTypeNoneDisablesSystemKeyboard {
311  NSDictionary* config = self.mutableTemplateCopy;
312  [config setValue:@{@"name" : @"TextInputType.none"} forKey:@"inputType"];
313  [self setClientId:123 configuration:config];
314 
315  // Verify the view's inputViewController is not nil;
316  XCTAssertNotNil(textInputPlugin.activeView.inputViewController);
317 
318  [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
319  [self setClientId:124 configuration:config];
320  XCTAssertNotNil(textInputPlugin.activeView);
321  XCTAssertNil(textInputPlugin.activeView.inputViewController);
322 }
323 
324 - (void)testAutocorrectionPromptRectAppearsBeforeIOS17AndDoesNotAppearAfterIOS17 {
325  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
326  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
327 
328  if (@available(iOS 17.0, *)) {
329  // Auto-correction prompt is disabled in iOS 17+.
330  OCMVerify(never(), [engine flutterTextInputView:inputView
331  showAutocorrectionPromptRectForStart:0
332  end:1
333  withClient:0]);
334  } else {
335  OCMVerify([engine flutterTextInputView:inputView
336  showAutocorrectionPromptRectForStart:0
337  end:1
338  withClient:0]);
339  }
340 }
341 
342 - (void)testIgnoresSelectionChangeIfSelectionIsDisabled {
343  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
344  __block int updateCount = 0;
345  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
346  .andDo(^(NSInvocation* invocation) {
347  updateCount++;
348  });
349 
350  [inputView.text setString:@"Some initial text"];
351  XCTAssertEqual(updateCount, 0);
352 
353  FlutterTextRange* textRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
354  [inputView setSelectedTextRange:textRange];
355  XCTAssertEqual(updateCount, 1);
356 
357  // Disable the interactive selection.
358  NSDictionary* config = self.mutableTemplateCopy;
359  [config setValue:@(NO) forKey:@"enableInteractiveSelection"];
360  [config setValue:@(NO) forKey:@"obscureText"];
361  [config setValue:@(NO) forKey:@"enableDeltaModel"];
362  [inputView configureWithDictionary:config];
363 
364  textRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(2, 3)];
365  [inputView setSelectedTextRange:textRange];
366  // The update count does not change.
367  XCTAssertEqual(updateCount, 1);
368 }
369 
370 - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
371  // Auto-correction prompt is disabled in iOS 17+.
372  if (@available(iOS 17.0, *)) {
373  return;
374  }
375 
376  if (@available(iOS 14.0, *)) {
377  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
378 
379  __block int callCount = 0;
380  OCMStub([engine flutterTextInputView:inputView
381  showAutocorrectionPromptRectForStart:0
382  end:1
383  withClient:0])
384  .andDo(^(NSInvocation* invocation) {
385  callCount++;
386  });
387 
388  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
389  // showAutocorrectionPromptRectForStart fires in response to firstRectForRange
390  XCTAssertEqual(callCount, 1);
391 
392  UIScribbleInteraction* scribbleInteraction =
393  [[UIScribbleInteraction alloc] initWithDelegate:inputView];
394 
395  [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
396  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
397  // showAutocorrectionPromptRectForStart does not fire in response to setMarkedText during a
398  // scribble interaction.firstRectForRange
399  XCTAssertEqual(callCount, 1);
400 
401  [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
402  [inputView resetScribbleInteractionStatusIfEnding];
403  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
404  // showAutocorrectionPromptRectForStart fires in response to firstRectForRange.
405  XCTAssertEqual(callCount, 2);
406 
407  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
408  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
409  // showAutocorrectionPromptRectForStart does not fire in response to firstRectForRange during a
410  // scribble-initiated focus.
411  XCTAssertEqual(callCount, 2);
412 
413  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
414  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
415  // showAutocorrectionPromptRectForStart does not fire in response to firstRectForRange after a
416  // scribble-initiated focus.
417  XCTAssertEqual(callCount, 2);
418 
419  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
420  [inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
421  // showAutocorrectionPromptRectForStart fires in response to firstRectForRange.
422  XCTAssertEqual(callCount, 3);
423  }
424 }
425 
426 - (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
427  FlutterTextInputPlugin* myInputPlugin =
428  [[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
429 
430  FlutterMethodCall* setClientCall =
431  [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
432  arguments:@[ @(123), self.mutableTemplateCopy ]];
433  [myInputPlugin handleMethodCall:setClientCall
434  result:^(id _Nullable result){
435  }];
436 
437  FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView);
438  OCMStub([mockInputView isScribbleAvailable]).andReturn(NO);
439 
440  // yOffset = 200.
441  NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
442 
443  FlutterMethodCall* setPlatformViewClientCall =
444  [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
445  arguments:@{@"transform" : yOffsetMatrix}];
446  [myInputPlugin handleMethodCall:setPlatformViewClientCall
447  result:^(id _Nullable result){
448  }];
449 
450  if (@available(iOS 17, *)) {
451  XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
452  @"The input hider should overlap with the text on and after iOS 17");
453 
454  } else {
455  XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
456  @"The input hider should be on the origin of screen on and before iOS 16.");
457  }
458 }
459 
460 - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
461  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
464 
465  FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition
466  toPosition:toPosition];
467  NSRange range = flutterRange.range;
468 
469  XCTAssertEqual(range.location, 0ul);
470  XCTAssertEqual(range.length, 2ul);
471 }
472 
473 - (void)testTextInRange {
474  NSDictionary* config = self.mutableTemplateCopy;
475  [config setValue:@{@"name" : @"TextInputType.url"} forKey:@"inputType"];
476  [self setClientId:123 configuration:config];
477  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
478  FlutterTextInputView* inputView = inputFields[0];
479 
480  [inputView insertText:@"test"];
481 
482  UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 20)];
483  NSString* substring = [inputView textInRange:range];
484  XCTAssertEqual(substring.length, 4ul);
485 
486  range = [FlutterTextRange rangeWithNSRange:NSMakeRange(10, 20)];
487  substring = [inputView textInRange:range];
488  XCTAssertEqual(substring.length, 0ul);
489 }
490 
491 - (void)testStandardEditActions {
492  NSDictionary* config = self.mutableTemplateCopy;
493  [self setClientId:123 configuration:config];
494  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
495  FlutterTextInputView* inputView = inputFields[0];
496 
497  [inputView insertText:@"aaaa"];
498  [inputView selectAll:nil];
499  [inputView cut:nil];
500  [inputView insertText:@"bbbb"];
501  XCTAssertTrue([inputView canPerformAction:@selector(paste:) withSender:nil]);
502  [inputView paste:nil];
503  [inputView selectAll:nil];
504  [inputView copy:nil];
505  [inputView paste:nil];
506  [inputView selectAll:nil];
507  [inputView delete:nil];
508  [inputView paste:nil];
509  [inputView paste:nil];
510 
511  UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 30)];
512  NSString* substring = [inputView textInRange:range];
513  XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa");
514 }
515 
516 - (void)testDeletingBackward {
517  NSDictionary* config = self.mutableTemplateCopy;
518  [self setClientId:123 configuration:config];
519  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
520  FlutterTextInputView* inputView = inputFields[0];
521 
522  [inputView insertText:@"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³à¸”ี "];
523  [inputView deleteBackward];
524  [inputView deleteBackward];
525 
526  // Thai vowel is removed.
527  XCTAssertEqualObjects(inputView.text, @"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³à¸”");
528  [inputView deleteBackward];
529  XCTAssertEqualObjects(inputView.text, @"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ‡ºðŸ‡³");
530  [inputView deleteBackward];
531  XCTAssertEqualObjects(inputView.text, @"ឹ😀 text 🥰👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦");
532  [inputView deleteBackward];
533  XCTAssertEqualObjects(inputView.text, @"ឹ😀 text 🥰");
534  [inputView deleteBackward];
535 
536  XCTAssertEqualObjects(inputView.text, @"ឹ😀 text ");
537  [inputView deleteBackward];
538  [inputView deleteBackward];
539  [inputView deleteBackward];
540  [inputView deleteBackward];
541  [inputView deleteBackward];
542  [inputView deleteBackward];
543 
544  XCTAssertEqualObjects(inputView.text, @"ឹ😀");
545  [inputView deleteBackward];
546  XCTAssertEqualObjects(inputView.text, @"áž¹");
547  [inputView deleteBackward];
548  XCTAssertEqualObjects(inputView.text, @"");
549 }
550 
551 // This tests the workaround to fix an iOS 16 bug
552 // See: https://github.com/flutter/flutter/issues/111494
553 - (void)testSystemOnlyAddingPartialComposedCharacter {
554  NSDictionary* config = self.mutableTemplateCopy;
555  [self setClientId:123 configuration:config];
556  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
557  FlutterTextInputView* inputView = inputFields[0];
558 
559  [inputView insertText:@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦"];
560  [inputView deleteBackward];
561 
562  // Insert the first unichar in the emoji.
563  [inputView insertText:[@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦" substringWithRange:NSMakeRange(0, 1)]];
564  [inputView insertText:@"ì•„"];
565 
566  XCTAssertEqualObjects(inputView.text, @"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ì•„");
567 
568  // Deleting ì•„.
569  [inputView deleteBackward];
570  // 👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ should be the current string.
571 
572  [inputView insertText:@"😀"];
573  [inputView deleteBackward];
574  // Insert the first unichar in the emoji.
575  [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
576  [inputView insertText:@"ì•„"];
577  XCTAssertEqualObjects(inputView.text, @"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
578 
579  // Deleting ì•„.
580  [inputView deleteBackward];
581  // 👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ should be the current string.
582 
583  [inputView deleteBackward];
584  // Insert the first unichar in the emoji.
585  [inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
586  [inputView insertText:@"ì•„"];
587 
588  XCTAssertEqualObjects(inputView.text, @"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦ðŸ˜€ì•„");
589 }
590 
591 - (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
592  NSDictionary* config = self.mutableTemplateCopy;
593  [self setClientId:123 configuration:config];
594  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
595  FlutterTextInputView* inputView = inputFields[0];
596 
597  [inputView insertText:@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦"];
598  [inputView deleteBackward];
599  [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];
600 
601  // Insert the first unichar in the emoji.
602  NSString* brokenEmoji = [@"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦" substringWithRange:NSMakeRange(0, 1)];
603  [inputView insertText:brokenEmoji];
604  [inputView insertText:@"ì•„"];
605 
606  NSString* finalText = [NSString stringWithFormat:@"%@ì•„", brokenEmoji];
607  XCTAssertEqualObjects(inputView.text, finalText);
608 }
609 
610 - (void)testPastingNonTextDisallowed {
611  NSDictionary* config = self.mutableTemplateCopy;
612  [self setClientId:123 configuration:config];
613  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
614  FlutterTextInputView* inputView = inputFields[0];
615 
616  UIPasteboard.generalPasteboard.color = UIColor.redColor;
617  XCTAssertNil(UIPasteboard.generalPasteboard.string);
618  XCTAssertFalse([inputView canPerformAction:@selector(paste:) withSender:nil]);
619  [inputView paste:nil];
620 
621  XCTAssertEqualObjects(inputView.text, @"");
622 }
623 
624 - (void)testNoZombies {
625  // Regression test for https://github.com/flutter/flutter/issues/62501.
626  FlutterSecureTextInputView* passwordView =
627  [[FlutterSecureTextInputView alloc] initWithOwner:textInputPlugin];
628 
629  @autoreleasepool {
630  // Initialize the lazy textField.
631  [passwordView.textField description];
632  }
633  XCTAssert([[passwordView.textField description] containsString:@"TextField"]);
634 }
635 
636 - (void)testInputViewCrash {
637  FlutterTextInputView* activeView = nil;
638  @autoreleasepool {
639  FlutterEngine* flutterEngine = [[FlutterEngine alloc] init];
640  FlutterTextInputPlugin* inputPlugin = [[FlutterTextInputPlugin alloc]
641  initWithDelegate:(id<FlutterTextInputDelegate>)flutterEngine];
642  activeView = inputPlugin.activeView;
643  }
644  [activeView updateEditingState];
645 }
646 
647 - (void)testDoNotReuseInputViews {
648  NSDictionary* config = self.mutableTemplateCopy;
649  [self setClientId:123 configuration:config];
650  FlutterTextInputView* currentView = textInputPlugin.activeView;
651  [self setClientId:456 configuration:config];
652 
653  XCTAssertNotNil(currentView);
654  XCTAssertNotNil(textInputPlugin.activeView);
655  XCTAssertNotEqual(currentView, textInputPlugin.activeView);
656 }
657 
658 - (void)ensureOnlyActiveViewCanBecomeFirstResponder {
659  for (FlutterTextInputView* inputView in self.installedInputViews) {
660  XCTAssertEqual(inputView.canBecomeFirstResponder, inputView == textInputPlugin.activeView);
661  }
662 }
663 
664 - (void)testPropagatePressEventsToViewController {
665  FlutterViewController* mockViewController = OCMPartialMock(viewController);
666  OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
667  OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
668 
669  textInputPlugin.viewController = mockViewController;
670 
671  NSDictionary* config = self.mutableTemplateCopy;
672  [self setClientId:123 configuration:config];
673  FlutterTextInputView* currentView = textInputPlugin.activeView;
674  [self setTextInputShow];
675 
676  [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
677  withEvent:OCMClassMock([UIPressesEvent class])];
678 
679  OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
680  withEvent:[OCMArg isNotNil]]);
681  OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
682  withEvent:[OCMArg isNotNil]]);
683 
684  [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
685  withEvent:OCMClassMock([UIPressesEvent class])];
686 
687  OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
688  withEvent:[OCMArg isNotNil]]);
689  OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
690  withEvent:[OCMArg isNotNil]]);
691 }
692 
693 - (void)testPropagatePressEventsToViewController2 {
694  FlutterViewController* mockViewController = OCMPartialMock(viewController);
695  OCMStub([mockViewController pressesBegan:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
696  OCMStub([mockViewController pressesEnded:[OCMArg isNotNil] withEvent:[OCMArg isNotNil]]);
697 
698  textInputPlugin.viewController = mockViewController;
699 
700  NSDictionary* config = self.mutableTemplateCopy;
701  [self setClientId:123 configuration:config];
702  [self setTextInputShow];
703  FlutterTextInputView* currentView = textInputPlugin.activeView;
704 
705  [currentView pressesBegan:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
706  withEvent:OCMClassMock([UIPressesEvent class])];
707 
708  OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
709  withEvent:[OCMArg isNotNil]]);
710  OCMVerify(times(0), [mockViewController pressesEnded:[OCMArg isNotNil]
711  withEvent:[OCMArg isNotNil]]);
712 
713  // Switch focus to a different view.
714  [self setClientId:321 configuration:config];
715  [self setTextInputShow];
716  NSAssert(textInputPlugin.activeView, @"active view must not be nil");
717  NSAssert(textInputPlugin.activeView != currentView, @"active view must change");
718  currentView = textInputPlugin.activeView;
719  [currentView pressesEnded:[NSSet setWithObjects:OCMClassMock([UIPress class]), nil]
720  withEvent:OCMClassMock([UIPressesEvent class])];
721 
722  OCMVerify(times(1), [mockViewController pressesBegan:[OCMArg isNotNil]
723  withEvent:[OCMArg isNotNil]]);
724  OCMVerify(times(1), [mockViewController pressesEnded:[OCMArg isNotNil]
725  withEvent:[OCMArg isNotNil]]);
726 }
727 
728 - (void)testUpdateSecureTextEntry {
729  NSDictionary* config = self.mutableTemplateCopy;
730  [config setValue:@"YES" forKey:@"obscureText"];
731  [self setClientId:123 configuration:config];
732 
733  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
734  FlutterTextInputView* inputView = OCMPartialMock(inputFields[0]);
735 
736  __block int callCount = 0;
737  OCMStub([inputView reloadInputViews]).andDo(^(NSInvocation* invocation) {
738  callCount++;
739  });
740 
741  XCTAssertTrue(inputView.isSecureTextEntry);
742 
743  config = self.mutableTemplateCopy;
744  [config setValue:@"NO" forKey:@"obscureText"];
745  [self updateConfig:config];
746 
747  XCTAssertEqual(callCount, 1);
748  XCTAssertFalse(inputView.isSecureTextEntry);
749 }
750 
751 - (void)testInputActionContinueAction {
752  id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]);
753  FlutterEngine* testEngine = [[FlutterEngine alloc] init];
754  [testEngine setBinaryMessenger:mockBinaryMessenger];
755  [testEngine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"];
756 
757  FlutterTextInputPlugin* inputPlugin =
758  [[FlutterTextInputPlugin alloc] initWithDelegate:(id<FlutterTextInputDelegate>)testEngine];
759  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:inputPlugin];
760 
761  [testEngine flutterTextInputView:inputView
762  performAction:FlutterTextInputActionContinue
763  withClient:123];
764 
765  FlutterMethodCall* methodCall =
766  [FlutterMethodCall methodCallWithMethodName:@"TextInputClient.performAction"
767  arguments:@[ @(123), @"TextInputAction.continueAction" ]];
768  NSData* encodedMethodCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:methodCall];
769  OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/textinput" message:encodedMethodCall]);
770 }
771 
772 - (void)testDisablingAutocorrectDisablesSpellChecking {
773  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
774 
775  // Disable the interactive selection.
776  NSDictionary* config = self.mutableTemplateCopy;
777  [inputView configureWithDictionary:config];
778 
779  XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeDefault);
780  XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeDefault);
781 
782  [config setValue:@(NO) forKey:@"autocorrect"];
783  [inputView configureWithDictionary:config];
784 
785  XCTAssertEqual(inputView.autocorrectionType, UITextAutocorrectionTypeNo);
786  XCTAssertEqual(inputView.spellCheckingType, UITextSpellCheckingTypeNo);
787 }
788 
789 #pragma mark - TextEditingDelta tests
790 - (void)testTextEditingDeltasAreGeneratedOnTextInput {
791  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
792  inputView.enableDeltaModel = YES;
793 
794  __block int updateCount = 0;
795 
796  [inputView insertText:@"text to insert"];
797  OCMExpect(
798  [engine
799  flutterTextInputView:inputView
800  updateEditingClient:0
801  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
802  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
803  isEqualToString:@""]) &&
804  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
805  isEqualToString:@"text to insert"]) &&
806  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) &&
807  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 0);
808  }]])
809  .andDo(^(NSInvocation* invocation) {
810  updateCount++;
811  });
812  XCTAssertEqual(updateCount, 0);
813 
814  [self flushScheduledAsyncBlocks];
815 
816  // Update the framework exactly once.
817  XCTAssertEqual(updateCount, 1);
818 
819  [inputView deleteBackward];
820  OCMExpect([engine flutterTextInputView:inputView
821  updateEditingClient:0
822  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
823  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
824  isEqualToString:@"text to insert"]) &&
825  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
826  isEqualToString:@""]) &&
827  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"]
828  intValue] == 13) &&
829  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"]
830  intValue] == 14);
831  }]])
832  .andDo(^(NSInvocation* invocation) {
833  updateCount++;
834  });
835  [self flushScheduledAsyncBlocks];
836  XCTAssertEqual(updateCount, 2);
837 
838  inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
839  OCMExpect([engine flutterTextInputView:inputView
840  updateEditingClient:0
841  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
842  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
843  isEqualToString:@"text to inser"]) &&
844  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
845  isEqualToString:@""]) &&
846  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"]
847  intValue] == -1) &&
848  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"]
849  intValue] == -1);
850  }]])
851  .andDo(^(NSInvocation* invocation) {
852  updateCount++;
853  });
854  [self flushScheduledAsyncBlocks];
855  XCTAssertEqual(updateCount, 3);
856 
857  [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]
858  withText:@"replace text"];
859  OCMExpect(
860  [engine
861  flutterTextInputView:inputView
862  updateEditingClient:0
863  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
864  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
865  isEqualToString:@"text to inser"]) &&
866  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
867  isEqualToString:@"replace text"]) &&
868  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 0) &&
869  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 1);
870  }]])
871  .andDo(^(NSInvocation* invocation) {
872  updateCount++;
873  });
874  [self flushScheduledAsyncBlocks];
875  XCTAssertEqual(updateCount, 4);
876 
877  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
878  OCMExpect([engine flutterTextInputView:inputView
879  updateEditingClient:0
880  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
881  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
882  isEqualToString:@"replace textext to inser"]) &&
883  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
884  isEqualToString:@"marked text"]) &&
885  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"]
886  intValue] == 12) &&
887  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"]
888  intValue] == 12);
889  }]])
890  .andDo(^(NSInvocation* invocation) {
891  updateCount++;
892  });
893  [self flushScheduledAsyncBlocks];
894  XCTAssertEqual(updateCount, 5);
895 
896  [inputView unmarkText];
897  OCMExpect([engine
898  flutterTextInputView:inputView
899  updateEditingClient:0
900  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
901  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
902  isEqualToString:@"replace textmarked textext to inser"]) &&
903  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
904  isEqualToString:@""]) &&
905  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] ==
906  -1) &&
907  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] ==
908  -1);
909  }]])
910  .andDo(^(NSInvocation* invocation) {
911  updateCount++;
912  });
913  [self flushScheduledAsyncBlocks];
914 
915  XCTAssertEqual(updateCount, 6);
916  OCMVerifyAll(engine);
917 }
918 
919 - (void)testTextEditingDeltasAreBatchedAndForwardedToFramework {
920  // Setup
921  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
922  inputView.enableDeltaModel = YES;
923 
924  // Expected call.
925  OCMExpect([engine flutterTextInputView:inputView
926  updateEditingClient:0
927  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
928  NSArray* deltas = state[@"deltas"];
929  NSDictionary* firstDelta = deltas[0];
930  NSDictionary* secondDelta = deltas[1];
931  NSDictionary* thirdDelta = deltas[2];
932  return [firstDelta[@"oldText"] isEqualToString:@""] &&
933  [firstDelta[@"deltaText"] isEqualToString:@"-"] &&
934  [firstDelta[@"deltaStart"] intValue] == 0 &&
935  [firstDelta[@"deltaEnd"] intValue] == 0 &&
936  [secondDelta[@"oldText"] isEqualToString:@"-"] &&
937  [secondDelta[@"deltaText"] isEqualToString:@""] &&
938  [secondDelta[@"deltaStart"] intValue] == 0 &&
939  [secondDelta[@"deltaEnd"] intValue] == 1 &&
940  [thirdDelta[@"oldText"] isEqualToString:@""] &&
941  [thirdDelta[@"deltaText"] isEqualToString:@"—"] &&
942  [thirdDelta[@"deltaStart"] intValue] == 0 &&
943  [thirdDelta[@"deltaEnd"] intValue] == 0;
944  }]]);
945 
946  // Simulate user input.
947  [inputView insertText:@"-"];
948  [inputView deleteBackward];
949  [inputView insertText:@"—"];
950 
951  [self flushScheduledAsyncBlocks];
952  OCMVerifyAll(engine);
953 }
954 
955 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextReplacement {
956  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
957  inputView.enableDeltaModel = YES;
958 
959  __block int updateCount = 0;
960  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
961  .andDo(^(NSInvocation* invocation) {
962  updateCount++;
963  });
964 
965  [inputView.text setString:@"Some initial text"];
966  XCTAssertEqual(updateCount, 0);
967 
968  UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(13, 4)];
969  inputView.markedTextRange = range;
970  inputView.selectedTextRange = nil;
971  [self flushScheduledAsyncBlocks];
972  XCTAssertEqual(updateCount, 1);
973 
974  [inputView setMarkedText:@"new marked text." selectedRange:NSMakeRange(0, 1)];
975  OCMVerify([engine
976  flutterTextInputView:inputView
977  updateEditingClient:0
978  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
979  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
980  isEqualToString:@"Some initial text"]) &&
981  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
982  isEqualToString:@"new marked text."]) &&
983  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) &&
984  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17);
985  }]]);
986  [self flushScheduledAsyncBlocks];
987  XCTAssertEqual(updateCount, 2);
988 }
989 
990 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextInsertion {
991  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
992  inputView.enableDeltaModel = YES;
993 
994  __block int updateCount = 0;
995  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
996  .andDo(^(NSInvocation* invocation) {
997  updateCount++;
998  });
999 
1000  [inputView.text setString:@"Some initial text"];
1001  [self flushScheduledAsyncBlocks];
1002  XCTAssertEqual(updateCount, 0);
1003 
1004  UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(13, 4)];
1005  inputView.markedTextRange = range;
1006  inputView.selectedTextRange = nil;
1007  [self flushScheduledAsyncBlocks];
1008  XCTAssertEqual(updateCount, 1);
1009 
1010  [inputView setMarkedText:@"text." selectedRange:NSMakeRange(0, 1)];
1011  OCMVerify([engine
1012  flutterTextInputView:inputView
1013  updateEditingClient:0
1014  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1015  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
1016  isEqualToString:@"Some initial text"]) &&
1017  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
1018  isEqualToString:@"text."]) &&
1019  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) &&
1020  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17);
1021  }]]);
1022  [self flushScheduledAsyncBlocks];
1023  XCTAssertEqual(updateCount, 2);
1024 }
1025 
1026 - (void)testTextEditingDeltasAreGeneratedOnSetMarkedTextDeletion {
1027  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1028  inputView.enableDeltaModel = YES;
1029 
1030  __block int updateCount = 0;
1031  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1032  .andDo(^(NSInvocation* invocation) {
1033  updateCount++;
1034  });
1035 
1036  [inputView.text setString:@"Some initial text"];
1037  [self flushScheduledAsyncBlocks];
1038  XCTAssertEqual(updateCount, 0);
1039 
1040  UITextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(13, 4)];
1041  inputView.markedTextRange = range;
1042  inputView.selectedTextRange = nil;
1043  [self flushScheduledAsyncBlocks];
1044  XCTAssertEqual(updateCount, 1);
1045 
1046  [inputView setMarkedText:@"tex" selectedRange:NSMakeRange(0, 1)];
1047  OCMVerify([engine
1048  flutterTextInputView:inputView
1049  updateEditingClient:0
1050  withDelta:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1051  return ([[state[@"deltas"] objectAtIndex:0][@"oldText"]
1052  isEqualToString:@"Some initial text"]) &&
1053  ([[state[@"deltas"] objectAtIndex:0][@"deltaText"]
1054  isEqualToString:@"tex"]) &&
1055  ([[state[@"deltas"] objectAtIndex:0][@"deltaStart"] intValue] == 13) &&
1056  ([[state[@"deltas"] objectAtIndex:0][@"deltaEnd"] intValue] == 17);
1057  }]]);
1058  [self flushScheduledAsyncBlocks];
1059  XCTAssertEqual(updateCount, 2);
1060 }
1061 
1062 #pragma mark - EditingState tests
1063 
1064 - (void)testUITextInputCallsUpdateEditingStateOnce {
1065  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1066 
1067  __block int updateCount = 0;
1068  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1069  .andDo(^(NSInvocation* invocation) {
1070  updateCount++;
1071  });
1072 
1073  [inputView insertText:@"text to insert"];
1074  // Update the framework exactly once.
1075  XCTAssertEqual(updateCount, 1);
1076 
1077  [inputView deleteBackward];
1078  XCTAssertEqual(updateCount, 2);
1079 
1080  inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
1081  XCTAssertEqual(updateCount, 3);
1082 
1083  [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]
1084  withText:@"replace text"];
1085  XCTAssertEqual(updateCount, 4);
1086 
1087  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1088  XCTAssertEqual(updateCount, 5);
1089 
1090  [inputView unmarkText];
1091  XCTAssertEqual(updateCount, 6);
1092 }
1093 
1094 - (void)testUITextInputCallsUpdateEditingStateWithDeltaOnce {
1095  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1096  inputView.enableDeltaModel = YES;
1097 
1098  __block int updateCount = 0;
1099  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1100  .andDo(^(NSInvocation* invocation) {
1101  updateCount++;
1102  });
1103 
1104  [inputView insertText:@"text to insert"];
1105  [self flushScheduledAsyncBlocks];
1106  // Update the framework exactly once.
1107  XCTAssertEqual(updateCount, 1);
1108 
1109  [inputView deleteBackward];
1110  [self flushScheduledAsyncBlocks];
1111  XCTAssertEqual(updateCount, 2);
1112 
1113  inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
1114  [self flushScheduledAsyncBlocks];
1115  XCTAssertEqual(updateCount, 3);
1116 
1117  [inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]
1118  withText:@"replace text"];
1119  [self flushScheduledAsyncBlocks];
1120  XCTAssertEqual(updateCount, 4);
1121 
1122  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1123  [self flushScheduledAsyncBlocks];
1124  XCTAssertEqual(updateCount, 5);
1125 
1126  [inputView unmarkText];
1127  [self flushScheduledAsyncBlocks];
1128  XCTAssertEqual(updateCount, 6);
1129 }
1130 
1131 - (void)testTextChangesDoNotTriggerUpdateEditingClient {
1132  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1133 
1134  __block int updateCount = 0;
1135  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1136  .andDo(^(NSInvocation* invocation) {
1137  updateCount++;
1138  });
1139 
1140  [inputView.text setString:@"BEFORE"];
1141  XCTAssertEqual(updateCount, 0);
1142 
1143  inputView.markedTextRange = nil;
1144  inputView.selectedTextRange = nil;
1145  XCTAssertEqual(updateCount, 1);
1146 
1147  // Text changes don't trigger an update.
1148  XCTAssertEqual(updateCount, 1);
1149  [inputView setTextInputState:@{@"text" : @"AFTER"}];
1150  XCTAssertEqual(updateCount, 1);
1151  [inputView setTextInputState:@{@"text" : @"AFTER"}];
1152  XCTAssertEqual(updateCount, 1);
1153 
1154  // Selection changes don't trigger an update.
1155  [inputView
1156  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1157  XCTAssertEqual(updateCount, 1);
1158  [inputView
1159  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1160  XCTAssertEqual(updateCount, 1);
1161 
1162  // Composing region changes don't trigger an update.
1163  [inputView
1164  setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1165  XCTAssertEqual(updateCount, 1);
1166  [inputView
1167  setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1168  XCTAssertEqual(updateCount, 1);
1169 }
1170 
1171 - (void)testTextChangesDoNotTriggerUpdateEditingClientWithDelta {
1172  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1173  inputView.enableDeltaModel = YES;
1174 
1175  __block int updateCount = 0;
1176  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withDelta:[OCMArg isNotNil]])
1177  .andDo(^(NSInvocation* invocation) {
1178  updateCount++;
1179  });
1180 
1181  [inputView.text setString:@"BEFORE"];
1182  [self flushScheduledAsyncBlocks];
1183  XCTAssertEqual(updateCount, 0);
1184 
1185  inputView.markedTextRange = nil;
1186  inputView.selectedTextRange = nil;
1187  [self flushScheduledAsyncBlocks];
1188  XCTAssertEqual(updateCount, 1);
1189 
1190  // Text changes don't trigger an update.
1191  XCTAssertEqual(updateCount, 1);
1192  [inputView setTextInputState:@{@"text" : @"AFTER"}];
1193  [self flushScheduledAsyncBlocks];
1194  XCTAssertEqual(updateCount, 1);
1195 
1196  [inputView setTextInputState:@{@"text" : @"AFTER"}];
1197  [self flushScheduledAsyncBlocks];
1198  XCTAssertEqual(updateCount, 1);
1199 
1200  // Selection changes don't trigger an update.
1201  [inputView
1202  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
1203  [self flushScheduledAsyncBlocks];
1204  XCTAssertEqual(updateCount, 1);
1205 
1206  [inputView
1207  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
1208  [self flushScheduledAsyncBlocks];
1209  XCTAssertEqual(updateCount, 1);
1210 
1211  // Composing region changes don't trigger an update.
1212  [inputView
1213  setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
1214  [self flushScheduledAsyncBlocks];
1215  XCTAssertEqual(updateCount, 1);
1216 
1217  [inputView
1218  setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1219  [self flushScheduledAsyncBlocks];
1220  XCTAssertEqual(updateCount, 1);
1221 }
1222 
1223 - (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {
1224  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1225 
1226  __block int updateCount = 0;
1227  OCMStub([engine flutterTextInputView:inputView updateEditingClient:0 withState:[OCMArg isNotNil]])
1228  .andDo(^(NSInvocation* invocation) {
1229  updateCount++;
1230  });
1231 
1232  [inputView unmarkText];
1233  // updateEditingClient shouldn't fire as the text is already unmarked.
1234  XCTAssertEqual(updateCount, 0);
1235 
1236  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1237  // updateEditingClient fires in response to setMarkedText.
1238  XCTAssertEqual(updateCount, 1);
1239 
1240  [inputView unmarkText];
1241  // updateEditingClient fires in response to unmarkText.
1242  XCTAssertEqual(updateCount, 2);
1243 }
1244 
1245 - (void)testCanCopyPasteWithScribbleEnabled {
1246  if (@available(iOS 14.0, *)) {
1247  NSDictionary* config = self.mutableTemplateCopy;
1248  [self setClientId:123 configuration:config];
1249  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
1250  FlutterTextInputView* inputView = inputFields[0];
1251 
1252  FlutterTextInputView* mockInputView = OCMPartialMock(inputView);
1253  OCMStub([mockInputView isScribbleAvailable]).andReturn(YES);
1254 
1255  [mockInputView insertText:@"aaaa"];
1256  [mockInputView selectAll:nil];
1257 
1258  XCTAssertFalse([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1259  XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:@"sender"]);
1260  XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1261  XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:@"sender"]);
1262 
1263  [mockInputView copy:NULL];
1264  XCTAssertFalse([mockInputView canPerformAction:@selector(copy:) withSender:NULL]);
1265  XCTAssertTrue([mockInputView canPerformAction:@selector(copy:) withSender:@"sender"]);
1266  XCTAssertFalse([mockInputView canPerformAction:@selector(paste:) withSender:NULL]);
1267  XCTAssertTrue([mockInputView canPerformAction:@selector(paste:) withSender:@"sender"]);
1268  }
1269 }
1270 
1271 - (void)testSetMarkedTextDuringScribbleDoesNotTriggerUpdateEditingClient {
1272  if (@available(iOS 14.0, *)) {
1273  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1274 
1275  __block int updateCount = 0;
1276  OCMStub([engine flutterTextInputView:inputView
1277  updateEditingClient:0
1278  withState:[OCMArg isNotNil]])
1279  .andDo(^(NSInvocation* invocation) {
1280  updateCount++;
1281  });
1282 
1283  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1284  // updateEditingClient fires in response to setMarkedText.
1285  XCTAssertEqual(updateCount, 1);
1286 
1287  UIScribbleInteraction* scribbleInteraction =
1288  [[UIScribbleInteraction alloc] initWithDelegate:inputView];
1289 
1290  [inputView scribbleInteractionWillBeginWriting:scribbleInteraction];
1291  [inputView setMarkedText:@"during writing" selectedRange:NSMakeRange(1, 2)];
1292  // updateEditingClient does not fire in response to setMarkedText during a scribble interaction.
1293  XCTAssertEqual(updateCount, 1);
1294 
1295  [inputView scribbleInteractionDidFinishWriting:scribbleInteraction];
1296  [inputView resetScribbleInteractionStatusIfEnding];
1297  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1298  // updateEditingClient fires in response to setMarkedText.
1299  XCTAssertEqual(updateCount, 2);
1300 
1301  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
1302  [inputView setMarkedText:@"during focus" selectedRange:NSMakeRange(1, 2)];
1303  // updateEditingClient does not fire in response to setMarkedText during a scribble-initiated
1304  // focus.
1305  XCTAssertEqual(updateCount, 2);
1306 
1307  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusFocused;
1308  [inputView setMarkedText:@"after focus" selectedRange:NSMakeRange(2, 3)];
1309  // updateEditingClient does not fire in response to setMarkedText after a scribble-initiated
1310  // focus.
1311  XCTAssertEqual(updateCount, 2);
1312 
1313  inputView.scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1314  [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
1315  // updateEditingClient fires in response to setMarkedText.
1316  XCTAssertEqual(updateCount, 3);
1317  }
1318 }
1319 
1320 - (void)testUpdateEditingClientNegativeSelection {
1321  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1322 
1323  [inputView.text setString:@"SELECTION"];
1324  inputView.markedTextRange = nil;
1325  inputView.selectedTextRange = nil;
1326 
1327  [inputView setTextInputState:@{
1328  @"text" : @"SELECTION",
1329  @"selectionBase" : @-1,
1330  @"selectionExtent" : @-1
1331  }];
1332  [inputView updateEditingState];
1333  OCMVerify([engine flutterTextInputView:inputView
1334  updateEditingClient:0
1335  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1336  return ([state[@"selectionBase"] intValue]) == 0 &&
1337  ([state[@"selectionExtent"] intValue] == 0);
1338  }]]);
1339 
1340  // Returns (0, 0) when either end goes below 0.
1341  [inputView
1342  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @-1, @"selectionExtent" : @1}];
1343  [inputView updateEditingState];
1344  OCMVerify([engine flutterTextInputView:inputView
1345  updateEditingClient:0
1346  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1347  return ([state[@"selectionBase"] intValue]) == 0 &&
1348  ([state[@"selectionExtent"] intValue] == 0);
1349  }]]);
1350 
1351  [inputView
1352  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @-1}];
1353  [inputView updateEditingState];
1354  OCMVerify([engine flutterTextInputView:inputView
1355  updateEditingClient:0
1356  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1357  return ([state[@"selectionBase"] intValue]) == 0 &&
1358  ([state[@"selectionExtent"] intValue] == 0);
1359  }]]);
1360 }
1361 
1362 - (void)testUpdateEditingClientSelectionClamping {
1363  // Regression test for https://github.com/flutter/flutter/issues/62992.
1364  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1365 
1366  [inputView.text setString:@"SELECTION"];
1367  inputView.markedTextRange = nil;
1368  inputView.selectedTextRange = nil;
1369 
1370  [inputView
1371  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @0}];
1372  [inputView updateEditingState];
1373  OCMVerify([engine flutterTextInputView:inputView
1374  updateEditingClient:0
1375  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1376  return ([state[@"selectionBase"] intValue]) == 0 &&
1377  ([state[@"selectionExtent"] intValue] == 0);
1378  }]]);
1379 
1380  // Needs clamping.
1381  [inputView setTextInputState:@{
1382  @"text" : @"SELECTION",
1383  @"selectionBase" : @0,
1384  @"selectionExtent" : @9999
1385  }];
1386  [inputView updateEditingState];
1387 
1388  OCMVerify([engine flutterTextInputView:inputView
1389  updateEditingClient:0
1390  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1391  return ([state[@"selectionBase"] intValue]) == 0 &&
1392  ([state[@"selectionExtent"] intValue] == 9);
1393  }]]);
1394 
1395  // No clamping needed, but in reverse direction.
1396  [inputView
1397  setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @0}];
1398  [inputView updateEditingState];
1399  OCMVerify([engine flutterTextInputView:inputView
1400  updateEditingClient:0
1401  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1402  return ([state[@"selectionBase"] intValue]) == 0 &&
1403  ([state[@"selectionExtent"] intValue] == 1);
1404  }]]);
1405 
1406  // Both ends need clamping.
1407  [inputView setTextInputState:@{
1408  @"text" : @"SELECTION",
1409  @"selectionBase" : @9999,
1410  @"selectionExtent" : @9999
1411  }];
1412  [inputView updateEditingState];
1413  OCMVerify([engine flutterTextInputView:inputView
1414  updateEditingClient:0
1415  withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
1416  return ([state[@"selectionBase"] intValue]) == 9 &&
1417  ([state[@"selectionExtent"] intValue] == 9);
1418  }]]);
1419 }
1420 
1421 - (void)testInputViewsHasNonNilInputDelegate {
1422  if (@available(iOS 13.0, *)) {
1423  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1424  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
1425 
1426  [inputView setTextInputClient:123];
1427  [inputView reloadInputViews];
1428  [inputView becomeFirstResponder];
1429  NSAssert(inputView.isFirstResponder, @"inputView is not first responder");
1430  inputView.inputDelegate = nil;
1431 
1432  FlutterTextInputView* mockInputView = OCMPartialMock(inputView);
1433  [mockInputView setTextInputState:@{
1434  @"text" : @"COMPOSING",
1435  @"composingBase" : @1,
1436  @"composingExtent" : @3
1437  }];
1438  OCMVerify([mockInputView setInputDelegate:[OCMArg isNotNil]]);
1439  [inputView removeFromSuperview];
1440  }
1441 }
1442 
1443 - (void)testInputViewsDoNotHaveUITextInteractions {
1444  if (@available(iOS 13.0, *)) {
1445  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1446  BOOL hasTextInteraction = NO;
1447  for (id interaction in inputView.interactions) {
1448  hasTextInteraction = [interaction isKindOfClass:[UITextInteraction class]];
1449  if (hasTextInteraction) {
1450  break;
1451  }
1452  }
1453  XCTAssertFalse(hasTextInteraction);
1454  }
1455 }
1456 
1457 #pragma mark - UITextInput methods - Tests
1458 
1459 - (void)testUpdateFirstRectForRange {
1460  [self setClientId:123 configuration:self.mutableTemplateCopy];
1461 
1462  FlutterTextInputView* inputView = textInputPlugin.activeView;
1463  textInputPlugin.viewController.view.frame = CGRectMake(0, 0, 0, 0);
1464 
1465  [inputView
1466  setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
1467 
1468  CGRect kInvalidFirstRect = CGRectMake(-1, -1, 9999, 9999);
1469  FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
1470  // yOffset = 200.
1471  NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
1472  NSArray* zeroMatrix = @[ @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0, @0 ];
1473  // This matrix can be generated by running this dart code snippet:
1474  // Matrix4.identity()..scale(3.0)..rotateZ(math.pi/2)..translate(1.0, 2.0,
1475  // 3.0);
1476  NSArray* affineMatrix = @[
1477  @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0), @(0.0), @(3.0), @(0.0),
1478  @(-6.0), @(3.0), @(9.0), @(1.0)
1479  ];
1480 
1481  // Invalid since we don't have the transform or the rect.
1482  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1483 
1484  [inputView setEditableTransform:yOffsetMatrix];
1485  // Invalid since we don't have the rect.
1486  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1487 
1488  // Valid rect and transform.
1489  CGRect testRect = CGRectMake(0, 0, 100, 100);
1490  [inputView setMarkedRect:testRect];
1491 
1492  CGRect finalRect = CGRectOffset(testRect, 0, 200);
1493  XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1494  // Idempotent.
1495  XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1496 
1497  // Use an invalid matrix:
1498  [inputView setEditableTransform:zeroMatrix];
1499  // Invalid matrix is invalid.
1500  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1501  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1502 
1503  // Revert the invalid matrix change.
1504  [inputView setEditableTransform:yOffsetMatrix];
1505  [inputView setMarkedRect:testRect];
1506  XCTAssertTrue(CGRectEqualToRect(finalRect, [inputView firstRectForRange:range]));
1507 
1508  // Use an invalid rect:
1509  [inputView setMarkedRect:kInvalidFirstRect];
1510  // Invalid marked rect is invalid.
1511  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1512  XCTAssertTrue(CGRectEqualToRect(kInvalidFirstRect, [inputView firstRectForRange:range]));
1513 
1514  // Use a 3d affine transform that does 3d-scaling, z-index rotating and 3d translation.
1515  [inputView setEditableTransform:affineMatrix];
1516  [inputView setMarkedRect:testRect];
1517  XCTAssertTrue(
1518  CGRectEqualToRect(CGRectMake(-306, 3, 300, 300), [inputView firstRectForRange:range]));
1519 
1520  NSAssert(inputView.superview, @"inputView is not in the view hierarchy!");
1521  const CGPoint offset = CGPointMake(113, 119);
1522  CGRect currentFrame = inputView.frame;
1523  currentFrame.origin = offset;
1524  inputView.frame = currentFrame;
1525  // Moving the input view within the FlutterView shouldn't affect the coordinates,
1526  // since the framework sends us global coordinates.
1527  XCTAssertTrue(CGRectEqualToRect(CGRectMake(-306 - 113, 3 - 119, 300, 300),
1528  [inputView firstRectForRange:range]));
1529 }
1530 
1531 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineLeftToRight {
1532  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1533  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1534 
1535  [inputView setSelectionRects:@[
1536  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1537  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1538  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1539  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1540  ]];
1541  FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1542  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1543  [inputView firstRectForRange:singleRectRange]));
1544 
1545  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1546 
1547  if (@available(iOS 17, *)) {
1548  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1549  [inputView firstRectForRange:multiRectRange]));
1550  } else {
1551  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1552  [inputView firstRectForRange:multiRectRange]));
1553  }
1554 
1555  [inputView setTextInputState:@{@"text" : @"COM"}];
1556  FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
1557  XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1558 }
1559 
1560 - (void)testFirstRectForRangeReturnsCorrectRectOnASingleLineRightToLeft {
1561  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1562  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1563 
1564  [inputView setSelectionRects:@[
1565  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1566  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1567  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1568  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1569  ]];
1570  FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1571  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1572  [inputView firstRectForRange:singleRectRange]));
1573 
1574  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1575  if (@available(iOS 17, *)) {
1576  XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1577  [inputView firstRectForRange:multiRectRange]));
1578  } else {
1579  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1580  [inputView firstRectForRange:multiRectRange]));
1581  }
1582 
1583  [inputView setTextInputState:@{@"text" : @"COM"}];
1584  FlutterTextRange* rangeOutsideBounds = [FlutterTextRange rangeWithNSRange:NSMakeRange(3, 1)];
1585  XCTAssertTrue(CGRectEqualToRect(CGRectZero, [inputView firstRectForRange:rangeOutsideBounds]));
1586 }
1587 
1588 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesLeftToRight {
1589  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1590  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1591 
1592  [inputView setSelectionRects:@[
1593  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1594  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1595  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1596  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1597  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:4U],
1598  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:5U],
1599  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:6U],
1600  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:7U],
1601  ]];
1602  FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1603  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1604  [inputView firstRectForRange:singleRectRange]));
1605 
1606  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1607 
1608  if (@available(iOS 17, *)) {
1609  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1610  [inputView firstRectForRange:multiRectRange]));
1611  } else {
1612  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1613  [inputView firstRectForRange:multiRectRange]));
1614  }
1615 }
1616 
1617 - (void)testFirstRectForRangeReturnsCorrectRectOnMultipleLinesRightToLeft {
1618  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1619  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1620 
1621  [inputView setSelectionRects:@[
1622  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1623  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1624  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1625  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1626  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 100, 100, 100) position:4U],
1627  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:5U],
1628  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:6U],
1629  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:7U],
1630  ]];
1631  FlutterTextRange* singleRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 1)];
1632  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1633  [inputView firstRectForRange:singleRectRange]));
1634 
1635  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1636  if (@available(iOS 17, *)) {
1637  XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1638  [inputView firstRectForRange:multiRectRange]));
1639  } else {
1640  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1641  [inputView firstRectForRange:multiRectRange]));
1642  }
1643 }
1644 
1645 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYLeftToRight {
1646  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1647  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1648 
1649  [inputView setSelectionRects:@[
1650  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1651  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1652  position:1U], // shorter
1653  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1654  position:2U], // taller
1655  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1656  ]];
1657 
1658  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1659 
1660  if (@available(iOS 17, *)) {
1661  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, -10, 300, 120),
1662  [inputView firstRectForRange:multiRectRange]));
1663  } else {
1664  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 10, 100, 80),
1665  [inputView firstRectForRange:multiRectRange]));
1666  }
1667 }
1668 
1669 - (void)testFirstRectForRangeReturnsCorrectRectOnSingleLineWithVaryingMinYAndMaxYRightToLeft {
1670  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1671  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1672 
1673  [inputView setSelectionRects:@[
1674  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1675  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, -10, 100, 120)
1676  position:1U], // taller
1677  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 10, 100, 80)
1678  position:2U], // shorter
1679  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1680  ]];
1681 
1682  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 3)];
1683 
1684  if (@available(iOS 17, *)) {
1685  XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, -10, 300, 120),
1686  [inputView firstRectForRange:multiRectRange]));
1687  } else {
1688  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, -10, 100, 120),
1689  [inputView firstRectForRange:multiRectRange]));
1690  }
1691 }
1692 
1693 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdLeftToRight {
1694  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1695  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1696 
1697  [inputView setSelectionRects:@[
1698  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1699  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1700  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1701  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1702  // y=60 exceeds threshold, so treat it as a new line.
1703  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 60, 100, 100) position:4U],
1704  ]];
1705 
1706  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1707 
1708  if (@available(iOS 17, *)) {
1709  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 300, 100),
1710  [inputView firstRectForRange:multiRectRange]));
1711  } else {
1712  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1713  [inputView firstRectForRange:multiRectRange]));
1714  }
1715 }
1716 
1717 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsExceedingThresholdRightToLeft {
1718  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1719  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1720 
1721  [inputView setSelectionRects:@[
1722  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:0U],
1723  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:1U],
1724  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:2U],
1725  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:3U],
1726  // y=60 exceeds threshold, so treat it as a new line.
1727  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 60, 100, 100) position:4U],
1728  ]];
1729 
1730  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1731 
1732  if (@available(iOS 17, *)) {
1733  XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 300, 100),
1734  [inputView firstRectForRange:multiRectRange]));
1735  } else {
1736  XCTAssertTrue(CGRectEqualToRect(CGRectMake(200, 0, 100, 100),
1737  [inputView firstRectForRange:multiRectRange]));
1738  }
1739 }
1740 
1741 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdLeftToRight {
1742  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1743  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1744 
1745  [inputView setSelectionRects:@[
1746  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1747  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:1U],
1748  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1749  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:3U],
1750  // y=40 is within line threshold, so treat it as the same line
1751  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 40, 100, 100) position:4U],
1752  ]];
1753 
1754  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1755 
1756  if (@available(iOS 17, *)) {
1757  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 400, 140),
1758  [inputView firstRectForRange:multiRectRange]));
1759  } else {
1760  XCTAssertTrue(CGRectEqualToRect(CGRectMake(100, 0, 100, 100),
1761  [inputView firstRectForRange:multiRectRange]));
1762  }
1763 }
1764 
1765 - (void)testFirstRectForRangeReturnsCorrectRectWithOverlappingRectsWithinThresholdRightToLeft {
1766  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1767  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1768 
1769  [inputView setSelectionRects:@[
1770  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(400, 0, 100, 100) position:0U],
1771  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 0, 100, 100) position:1U],
1772  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1773  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100) position:3U],
1774  // y=40 is within line threshold, so treat it as the same line
1775  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 40, 100, 100) position:4U],
1776  ]];
1777 
1778  FlutterTextRange* multiRectRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 4)];
1779 
1780  if (@available(iOS 17, *)) {
1781  XCTAssertTrue(CGRectEqualToRect(CGRectMake(0, 0, 400, 140),
1782  [inputView firstRectForRange:multiRectRange]));
1783  } else {
1784  XCTAssertTrue(CGRectEqualToRect(CGRectMake(300, 0, 100, 100),
1785  [inputView firstRectForRange:multiRectRange]));
1786  }
1787 }
1788 
1789 - (void)testClosestPositionToPoint {
1790  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1791  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1792 
1793  // Minimize the vertical distance from the center of the rects first
1794  [inputView setSelectionRects:@[
1795  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1796  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1797  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:2U],
1798  ]];
1799  CGPoint point = CGPointMake(150, 150);
1800  XCTAssertEqual(2U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1801  XCTAssertEqual(UITextStorageDirectionBackward,
1802  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1803 
1804  // Then, if the point is above the bottom of the closest rects vertically, get the closest x
1805  // origin
1806  [inputView setSelectionRects:@[
1807  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1808  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1809  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U],
1810  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U],
1811  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U],
1812  ]];
1813  point = CGPointMake(125, 150);
1814  XCTAssertEqual(2U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1815  XCTAssertEqual(UITextStorageDirectionForward,
1816  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1817 
1818  // However, if the point is below the bottom of the closest rects vertically, get the position
1819  // farthest to the right
1820  [inputView setSelectionRects:@[
1821  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1822  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1823  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U],
1824  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U],
1825  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 300, 100, 100) position:4U],
1826  ]];
1827  point = CGPointMake(125, 201);
1828  XCTAssertEqual(4U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1829  XCTAssertEqual(UITextStorageDirectionBackward,
1830  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1831 
1832  // Also check a point at the right edge of the last selection rect
1833  [inputView setSelectionRects:@[
1834  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1835  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1836  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U],
1837  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U],
1838  ]];
1839  point = CGPointMake(125, 250);
1840  XCTAssertEqual(4U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1841  XCTAssertEqual(UITextStorageDirectionBackward,
1842  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1843 
1844  // Minimize vertical distance if the difference is more than 1 point.
1845  [inputView setSelectionRects:@[
1846  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 2, 100, 100) position:0U],
1847  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 2, 100, 100) position:1U],
1848  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100) position:2U],
1849  ]];
1850  point = CGPointMake(110, 50);
1851  XCTAssertEqual(2U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1852  XCTAssertEqual(UITextStorageDirectionForward,
1853  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1854 
1855  // In floating cursor mode, the vertical difference is allowed to be 10 points.
1856  // The closest horizontal position will now win.
1857  [inputView beginFloatingCursorAtPoint:CGPointZero];
1858  XCTAssertEqual(1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).index);
1859  XCTAssertEqual(UITextStorageDirectionForward,
1860  ((FlutterTextPosition*)[inputView closestPositionToPoint:point]).affinity);
1861  [inputView endFloatingCursor];
1862 }
1863 
1864 - (void)testClosestPositionToPointRTL {
1865  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1866  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1867 
1868  [inputView setSelectionRects:@[
1869  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 0, 100, 100)
1870  position:0U
1871  writingDirection:NSWritingDirectionRightToLeft],
1872  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 0, 100, 100)
1873  position:1U
1874  writingDirection:NSWritingDirectionRightToLeft],
1875  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100)
1876  position:2U
1877  writingDirection:NSWritingDirectionRightToLeft],
1878  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100)
1879  position:3U
1880  writingDirection:NSWritingDirectionRightToLeft],
1881  ]];
1882  FlutterTextPosition* position =
1883  (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(275, 50)];
1884  XCTAssertEqual(0U, position.index);
1885  XCTAssertEqual(UITextStorageDirectionForward, position.affinity);
1886  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(225, 50)];
1887  XCTAssertEqual(1U, position.index);
1888  XCTAssertEqual(UITextStorageDirectionBackward, position.affinity);
1889  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(175, 50)];
1890  XCTAssertEqual(1U, position.index);
1891  XCTAssertEqual(UITextStorageDirectionForward, position.affinity);
1892  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(125, 50)];
1893  XCTAssertEqual(2U, position.index);
1894  XCTAssertEqual(UITextStorageDirectionBackward, position.affinity);
1895  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(75, 50)];
1896  XCTAssertEqual(2U, position.index);
1897  XCTAssertEqual(UITextStorageDirectionForward, position.affinity);
1898  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(25, 50)];
1899  XCTAssertEqual(3U, position.index);
1900  XCTAssertEqual(UITextStorageDirectionBackward, position.affinity);
1901  position = (FlutterTextPosition*)[inputView closestPositionToPoint:CGPointMake(-25, 50)];
1902  XCTAssertEqual(3U, position.index);
1903  XCTAssertEqual(UITextStorageDirectionBackward, position.affinity);
1904 }
1905 
1906 - (void)testSelectionRectsForRange {
1907  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1908  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1909 
1910  CGRect testRect0 = CGRectMake(100, 100, 100, 100);
1911  CGRect testRect1 = CGRectMake(200, 200, 100, 100);
1912  [inputView setSelectionRects:@[
1913  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1916  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 300, 100, 100) position:3U],
1917  ]];
1918 
1919  // Returns the matching rects within a range
1920  FlutterTextRange* range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 2)];
1921  XCTAssertTrue(CGRectEqualToRect(testRect0, [inputView selectionRectsForRange:range][0].rect));
1922  XCTAssertTrue(CGRectEqualToRect(testRect1, [inputView selectionRectsForRange:range][1].rect));
1923  XCTAssertEqual(2U, [[inputView selectionRectsForRange:range] count]);
1924 
1925  // Returns a 0 width rect for a 0-length range
1926  range = [FlutterTextRange rangeWithNSRange:NSMakeRange(1, 0)];
1927  XCTAssertEqual(1U, [[inputView selectionRectsForRange:range] count]);
1928  XCTAssertTrue(CGRectEqualToRect(
1929  CGRectMake(testRect0.origin.x, testRect0.origin.y, 0, testRect0.size.height),
1930  [inputView selectionRectsForRange:range][0].rect));
1931 }
1932 
1933 - (void)testClosestPositionToPointWithinRange {
1934  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1935  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1936 
1937  // Do not return a position before the start of the range
1938  [inputView setSelectionRects:@[
1939  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1940  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1941  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U],
1942  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U],
1943  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U],
1944  ]];
1945  CGPoint point = CGPointMake(125, 150);
1946  FlutterTextRange* range = [[FlutterTextRange rangeWithNSRange:NSMakeRange(3, 2)] copy];
1947  XCTAssertEqual(
1948  3U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
1949  XCTAssertEqual(
1950  UITextStorageDirectionForward,
1951  ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
1952 
1953  // Do not return a position after the end of the range
1954  [inputView setSelectionRects:@[
1955  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U],
1956  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 100, 100, 100) position:1U],
1957  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:2U],
1958  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 100, 100, 100) position:3U],
1959  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 200, 100, 100) position:4U],
1960  ]];
1961  point = CGPointMake(125, 150);
1962  range = [[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)] copy];
1963  XCTAssertEqual(
1964  1U, ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).index);
1965  XCTAssertEqual(
1966  UITextStorageDirectionForward,
1967  ((FlutterTextPosition*)[inputView closestPositionToPoint:point withinRange:range]).affinity);
1968 }
1969 
1970 - (void)testClosestPositionToPointWithPartialSelectionRects {
1971  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1972  [inputView setTextInputState:@{@"text" : @"COMPOSING"}];
1973 
1974  [inputView setSelectionRects:@[ [FlutterTextSelectionRect
1975  selectionRectWithRect:CGRectMake(0, 0, 100, 100)
1976  position:0U] ]];
1977  // Asking with a position at the end of selection rects should give you the trailing edge of
1978  // the last rect.
1979  XCTAssertTrue(CGRectEqualToRect(
1980  [inputView caretRectForPosition:[FlutterTextPosition
1981  positionWithIndex:1
1982  affinity:UITextStorageDirectionForward]],
1983  CGRectMake(100, 0, 0, 100)));
1984  // Asking with a position beyond the end of selection rects should return CGRectZero without
1985  // crashing.
1986  XCTAssertTrue(CGRectEqualToRect(
1987  [inputView caretRectForPosition:[FlutterTextPosition
1988  positionWithIndex:2
1989  affinity:UITextStorageDirectionForward]],
1990  CGRectZero));
1991 }
1992 
1993 #pragma mark - Floating Cursor - Tests
1994 
1995 - (void)testFloatingCursorDoesNotThrow {
1996  // The keyboard implementation may send unbalanced calls to the input view.
1997  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
1998  [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
1999  [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2000  [inputView endFloatingCursor];
2001  [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2002  [inputView endFloatingCursor];
2003 }
2004 
2005 - (void)testFloatingCursor {
2006  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2007  [inputView setTextInputState:@{
2008  @"text" : @"test",
2009  @"selectionBase" : @1,
2010  @"selectionExtent" : @1,
2011  }];
2012 
2013  FlutterTextSelectionRect* first =
2014  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U];
2015  FlutterTextSelectionRect* second =
2016  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:1U];
2017  FlutterTextSelectionRect* third =
2018  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U];
2019  FlutterTextSelectionRect* fourth =
2020  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 300, 100, 100) position:3U];
2021  [inputView setSelectionRects:@[ first, second, third, fourth ]];
2022 
2023  // Verify zeroth caret rect is based on left edge of first character.
2024  XCTAssertTrue(CGRectEqualToRect(
2025  [inputView caretRectForPosition:[FlutterTextPosition
2026  positionWithIndex:0
2027  affinity:UITextStorageDirectionForward]],
2028  CGRectMake(0, 0, 0, 100)));
2029  // Since the textAffinity is downstream, the caret rect will be based on the
2030  // left edge of the succeeding character.
2031  XCTAssertTrue(CGRectEqualToRect(
2032  [inputView caretRectForPosition:[FlutterTextPosition
2033  positionWithIndex:1
2034  affinity:UITextStorageDirectionForward]],
2035  CGRectMake(100, 100, 0, 100)));
2036  XCTAssertTrue(CGRectEqualToRect(
2037  [inputView caretRectForPosition:[FlutterTextPosition
2038  positionWithIndex:2
2039  affinity:UITextStorageDirectionForward]],
2040  CGRectMake(200, 200, 0, 100)));
2041  XCTAssertTrue(CGRectEqualToRect(
2042  [inputView caretRectForPosition:[FlutterTextPosition
2043  positionWithIndex:3
2044  affinity:UITextStorageDirectionForward]],
2045  CGRectMake(300, 300, 0, 100)));
2046  // There is no subsequent character for the last position, so the caret rect
2047  // will be based on the right edge of the preceding character.
2048  XCTAssertTrue(CGRectEqualToRect(
2049  [inputView caretRectForPosition:[FlutterTextPosition
2050  positionWithIndex:4
2051  affinity:UITextStorageDirectionForward]],
2052  CGRectMake(400, 300, 0, 100)));
2053  // Verify no caret rect for out-of-range character.
2054  XCTAssertTrue(CGRectEqualToRect(
2055  [inputView caretRectForPosition:[FlutterTextPosition
2056  positionWithIndex:5
2057  affinity:UITextStorageDirectionForward]],
2058  CGRectZero));
2059 
2060  // Check caret rects again again when text affinity is upstream.
2061  [inputView setTextInputState:@{
2062  @"text" : @"test",
2063  @"selectionBase" : @2,
2064  @"selectionExtent" : @2,
2065  }];
2066  // Verify zeroth caret rect is based on left edge of first character.
2067  XCTAssertTrue(CGRectEqualToRect(
2068  [inputView caretRectForPosition:[FlutterTextPosition
2069  positionWithIndex:0
2070  affinity:UITextStorageDirectionBackward]],
2071  CGRectMake(0, 0, 0, 100)));
2072  // Since the textAffinity is upstream, all below caret rects will be based on
2073  // the right edge of the preceding character.
2074  XCTAssertTrue(CGRectEqualToRect(
2075  [inputView caretRectForPosition:[FlutterTextPosition
2076  positionWithIndex:1
2077  affinity:UITextStorageDirectionBackward]],
2078  CGRectMake(100, 0, 0, 100)));
2079  XCTAssertTrue(CGRectEqualToRect(
2080  [inputView caretRectForPosition:[FlutterTextPosition
2081  positionWithIndex:2
2082  affinity:UITextStorageDirectionBackward]],
2083  CGRectMake(200, 100, 0, 100)));
2084  XCTAssertTrue(CGRectEqualToRect(
2085  [inputView caretRectForPosition:[FlutterTextPosition
2086  positionWithIndex:3
2087  affinity:UITextStorageDirectionBackward]],
2088  CGRectMake(300, 200, 0, 100)));
2089  XCTAssertTrue(CGRectEqualToRect(
2090  [inputView caretRectForPosition:[FlutterTextPosition
2091  positionWithIndex:4
2092  affinity:UITextStorageDirectionBackward]],
2093  CGRectMake(400, 300, 0, 100)));
2094  // Verify no caret rect for out-of-range character.
2095  XCTAssertTrue(CGRectEqualToRect(
2096  [inputView caretRectForPosition:[FlutterTextPosition
2097  positionWithIndex:5
2098  affinity:UITextStorageDirectionBackward]],
2099  CGRectZero));
2100 
2101  // Verify floating cursor updates are relative to original position, and that there is no bounds
2102  // change.
2103  CGRect initialBounds = inputView.bounds;
2104  [inputView beginFloatingCursorAtPoint:CGPointMake(123, 321)];
2105  XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2106  OCMVerify([engine flutterTextInputView:inputView
2107  updateFloatingCursor:FlutterFloatingCursorDragStateStart
2108  withClient:0
2109  withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2110  return ([state[@"X"] isEqualToNumber:@(0)]) &&
2111  ([state[@"Y"] isEqualToNumber:@(0)]);
2112  }]]);
2113 
2114  [inputView updateFloatingCursorAtPoint:CGPointMake(456, 654)];
2115  XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2116  OCMVerify([engine flutterTextInputView:inputView
2117  updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2118  withClient:0
2119  withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2120  return ([state[@"X"] isEqualToNumber:@(333)]) &&
2121  ([state[@"Y"] isEqualToNumber:@(333)]);
2122  }]]);
2123 
2124  [inputView endFloatingCursor];
2125  XCTAssertTrue(CGRectEqualToRect(initialBounds, inputView.bounds));
2126  OCMVerify([engine flutterTextInputView:inputView
2127  updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2128  withClient:0
2129  withPosition:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
2130  return ([state[@"X"] isEqualToNumber:@(0)]) &&
2131  ([state[@"Y"] isEqualToNumber:@(0)]);
2132  }]]);
2133 }
2134 
2135 #pragma mark - UIKeyInput Overrides - Tests
2136 
2137 - (void)testInsertTextAddsPlaceholderSelectionRects {
2138  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2139  [inputView
2140  setTextInputState:@{@"text" : @"test", @"selectionBase" : @1, @"selectionExtent" : @1}];
2141 
2142  FlutterTextSelectionRect* first =
2143  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(0, 0, 100, 100) position:0U];
2144  FlutterTextSelectionRect* second =
2145  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(100, 100, 100, 100) position:1U];
2146  FlutterTextSelectionRect* third =
2147  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(200, 200, 100, 100) position:2U];
2148  FlutterTextSelectionRect* fourth =
2149  [FlutterTextSelectionRect selectionRectWithRect:CGRectMake(300, 300, 100, 100) position:3U];
2150  [inputView setSelectionRects:@[ first, second, third, fourth ]];
2151 
2152  // Inserts additional selection rects at the selection start
2153  [inputView insertText:@"in"];
2154  NSArray* selectionRects =
2155  [inputView selectionRectsForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 6)]];
2156  XCTAssertEqual(6U, [selectionRects count]);
2157 
2158  XCTAssertEqual(first.position, ((FlutterTextSelectionRect*)selectionRects[0]).position);
2159  XCTAssertTrue(CGRectEqualToRect(first.rect, ((FlutterTextSelectionRect*)selectionRects[0]).rect));
2160 
2161  XCTAssertEqual(second.position, ((FlutterTextSelectionRect*)selectionRects[1]).position);
2162  XCTAssertTrue(
2163  CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[1]).rect));
2164 
2165  XCTAssertEqual(second.position + 1, ((FlutterTextSelectionRect*)selectionRects[2]).position);
2166  XCTAssertTrue(
2167  CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[2]).rect));
2168 
2169  XCTAssertEqual(second.position + 2, ((FlutterTextSelectionRect*)selectionRects[3]).position);
2170  XCTAssertTrue(
2171  CGRectEqualToRect(second.rect, ((FlutterTextSelectionRect*)selectionRects[3]).rect));
2172 
2173  XCTAssertEqual(third.position + 2, ((FlutterTextSelectionRect*)selectionRects[4]).position);
2174  XCTAssertTrue(CGRectEqualToRect(third.rect, ((FlutterTextSelectionRect*)selectionRects[4]).rect));
2175 
2176  XCTAssertEqual(fourth.position + 2, ((FlutterTextSelectionRect*)selectionRects[5]).position);
2177  XCTAssertTrue(
2178  CGRectEqualToRect(fourth.rect, ((FlutterTextSelectionRect*)selectionRects[5]).rect));
2179 }
2180 
2181 #pragma mark - Autofill - Utilities
2182 
2183 - (NSMutableDictionary*)mutablePasswordTemplateCopy {
2184  if (!_passwordTemplate) {
2185  _passwordTemplate = @{
2186  @"inputType" : @{@"name" : @"TextInuptType.text"},
2187  @"keyboardAppearance" : @"Brightness.light",
2188  @"obscureText" : @YES,
2189  @"inputAction" : @"TextInputAction.unspecified",
2190  @"smartDashesType" : @"0",
2191  @"smartQuotesType" : @"0",
2192  @"autocorrect" : @YES
2193  };
2194  }
2195 
2196  return [_passwordTemplate mutableCopy];
2197 }
2198 
2199 - (NSArray<FlutterTextInputView*>*)viewsVisibleToAutofill {
2200  return [self.installedInputViews
2201  filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"isVisibleToAutofill == YES"]];
2202 }
2203 
2204 - (void)commitAutofillContextAndVerify {
2205  FlutterMethodCall* methodCall =
2206  [FlutterMethodCall methodCallWithMethodName:@"TextInput.finishAutofillContext"
2207  arguments:@YES];
2208  [textInputPlugin handleMethodCall:methodCall
2209  result:^(id _Nullable result){
2210  }];
2211 
2212  XCTAssertEqual(self.viewsVisibleToAutofill.count,
2213  [textInputPlugin.activeView isVisibleToAutofill] ? 1ul : 0ul);
2214  XCTAssertNotEqual(textInputPlugin.textInputView, nil);
2215  // The active view should still be installed so it doesn't get
2216  // deallocated.
2217  XCTAssertEqual(self.installedInputViews.count, 1ul);
2218  XCTAssertEqual(textInputPlugin.autofillContext.count, 0ul);
2219 }
2220 
2221 #pragma mark - Autofill - Tests
2222 
2223 - (void)testDisablingAutofillOnInputClient {
2224  NSDictionary* config = self.mutableTemplateCopy;
2225  [config setValue:@"YES" forKey:@"obscureText"];
2226 
2227  [self setClientId:123 configuration:config];
2228 
2229  FlutterTextInputView* inputView = self.installedInputViews[0];
2230  XCTAssertEqualObjects(inputView.textContentType, @"");
2231 }
2232 
2233 - (void)testAutofillEnabledByDefault {
2234  NSDictionary* config = self.mutableTemplateCopy;
2235  [config setValue:@"NO" forKey:@"obscureText"];
2236  [config setValue:@{@"uniqueIdentifier" : @"field1", @"editingValue" : @{@"text" : @""}}
2237  forKey:@"autofill"];
2238 
2239  [self setClientId:123 configuration:config];
2240 
2241  FlutterTextInputView* inputView = self.installedInputViews[0];
2242  XCTAssertNil(inputView.textContentType);
2243 }
2244 
2245 - (void)testAutofillContext {
2246  NSMutableDictionary* field1 = self.mutableTemplateCopy;
2247 
2248  [field1 setValue:@{
2249  @"uniqueIdentifier" : @"field1",
2250  @"hints" : @[ @"hint1" ],
2251  @"editingValue" : @{@"text" : @""}
2252  }
2253  forKey:@"autofill"];
2254 
2255  NSMutableDictionary* field2 = self.mutablePasswordTemplateCopy;
2256  [field2 setValue:@{
2257  @"uniqueIdentifier" : @"field2",
2258  @"hints" : @[ @"hint2" ],
2259  @"editingValue" : @{@"text" : @""}
2260  }
2261  forKey:@"autofill"];
2262 
2263  NSMutableDictionary* config = [field1 mutableCopy];
2264  [config setValue:@[ field1, field2 ] forKey:@"fields"];
2265 
2266  [self setClientId:123 configuration:config];
2267  XCTAssertEqual(self.viewsVisibleToAutofill.count, 2ul);
2268 
2269  XCTAssertEqual(textInputPlugin.autofillContext.count, 2ul);
2270 
2271  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2272  XCTAssertEqual(self.installedInputViews.count, 2ul);
2273  XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]);
2274  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2275 
2276  // The configuration changes.
2277  NSMutableDictionary* field3 = self.mutablePasswordTemplateCopy;
2278  [field3 setValue:@{
2279  @"uniqueIdentifier" : @"field3",
2280  @"hints" : @[ @"hint3" ],
2281  @"editingValue" : @{@"text" : @""}
2282  }
2283  forKey:@"autofill"];
2284 
2285  NSMutableDictionary* oldContext = textInputPlugin.autofillContext;
2286  // Replace field2 with field3.
2287  [config setValue:@[ field1, field3 ] forKey:@"fields"];
2288 
2289  [self setClientId:123 configuration:config];
2290 
2291  XCTAssertEqual(self.viewsVisibleToAutofill.count, 2ul);
2292  XCTAssertEqual(textInputPlugin.autofillContext.count, 3ul);
2293 
2294  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2295  XCTAssertEqual(self.installedInputViews.count, 3ul);
2296  XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]);
2297  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2298 
2299  // Old autofill input fields are still installed and reused.
2300  for (NSString* key in oldContext.allKeys) {
2301  XCTAssertEqual(oldContext[key], textInputPlugin.autofillContext[key]);
2302  }
2303 
2304  // Switch to a password field that has no contentType and is not in an AutofillGroup.
2305  config = self.mutablePasswordTemplateCopy;
2306 
2307  oldContext = textInputPlugin.autofillContext;
2308  [self setClientId:124 configuration:config];
2309  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2310 
2311  XCTAssertEqual(self.viewsVisibleToAutofill.count, 1ul);
2312  XCTAssertEqual(textInputPlugin.autofillContext.count, 3ul);
2313 
2314  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2315  XCTAssertEqual(self.installedInputViews.count, 4ul);
2316 
2317  // Old autofill input fields are still installed and reused.
2318  for (NSString* key in oldContext.allKeys) {
2319  XCTAssertEqual(oldContext[key], textInputPlugin.autofillContext[key]);
2320  }
2321  // The active view should change.
2322  XCTAssertNotEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]);
2323  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2324 
2325  // Switch to a similar password field, the previous field should be reused.
2326  oldContext = textInputPlugin.autofillContext;
2327  [self setClientId:200 configuration:config];
2328 
2329  // Reuse the input view instance from the last time.
2330  XCTAssertEqual(self.viewsVisibleToAutofill.count, 1ul);
2331  XCTAssertEqual(textInputPlugin.autofillContext.count, 3ul);
2332 
2333  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2334  XCTAssertEqual(self.installedInputViews.count, 4ul);
2335 
2336  // Old autofill input fields are still installed and reused.
2337  for (NSString* key in oldContext.allKeys) {
2338  XCTAssertEqual(oldContext[key], textInputPlugin.autofillContext[key]);
2339  }
2340  XCTAssertNotEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]);
2341  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2342 }
2343 
2344 - (void)testCommitAutofillContext {
2345  NSMutableDictionary* field1 = self.mutableTemplateCopy;
2346  [field1 setValue:@{
2347  @"uniqueIdentifier" : @"field1",
2348  @"hints" : @[ @"hint1" ],
2349  @"editingValue" : @{@"text" : @""}
2350  }
2351  forKey:@"autofill"];
2352 
2353  NSMutableDictionary* field2 = self.mutablePasswordTemplateCopy;
2354  [field2 setValue:@{
2355  @"uniqueIdentifier" : @"field2",
2356  @"hints" : @[ @"hint2" ],
2357  @"editingValue" : @{@"text" : @""}
2358  }
2359  forKey:@"autofill"];
2360 
2361  NSMutableDictionary* field3 = self.mutableTemplateCopy;
2362  [field3 setValue:@{
2363  @"uniqueIdentifier" : @"field3",
2364  @"hints" : @[ @"hint3" ],
2365  @"editingValue" : @{@"text" : @""}
2366  }
2367  forKey:@"autofill"];
2368 
2369  NSMutableDictionary* config = [field1 mutableCopy];
2370  [config setValue:@[ field1, field2 ] forKey:@"fields"];
2371 
2372  [self setClientId:123 configuration:config];
2373  XCTAssertEqual(self.viewsVisibleToAutofill.count, 2ul);
2374  XCTAssertEqual(textInputPlugin.autofillContext.count, 2ul);
2375  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2376 
2377  [self commitAutofillContextAndVerify];
2378  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2379 
2380  // Install the password field again.
2381  [self setClientId:123 configuration:config];
2382  // Switch to a regular autofill group.
2383  [self setClientId:124 configuration:field3];
2384  XCTAssertEqual(self.viewsVisibleToAutofill.count, 1ul);
2385 
2386  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2387  XCTAssertEqual(self.installedInputViews.count, 3ul);
2388  XCTAssertEqual(textInputPlugin.autofillContext.count, 2ul);
2389  XCTAssertNotEqual(textInputPlugin.textInputView, nil);
2390  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2391 
2392  [self commitAutofillContextAndVerify];
2393  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2394 
2395  // Now switch to an input field that does not autofill.
2396  [self setClientId:125 configuration:self.mutableTemplateCopy];
2397 
2398  XCTAssertEqual(self.viewsVisibleToAutofill.count, 0ul);
2399  // The active view should still be installed so it doesn't get
2400  // deallocated.
2401 
2402  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2403  XCTAssertEqual(self.installedInputViews.count, 1ul);
2404  XCTAssertEqual(textInputPlugin.autofillContext.count, 0ul);
2405  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2406 
2407  [self commitAutofillContextAndVerify];
2408  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2409 }
2410 
2411 - (void)testAutofillInputViews {
2412  NSMutableDictionary* field1 = self.mutableTemplateCopy;
2413  [field1 setValue:@{
2414  @"uniqueIdentifier" : @"field1",
2415  @"hints" : @[ @"hint1" ],
2416  @"editingValue" : @{@"text" : @""}
2417  }
2418  forKey:@"autofill"];
2419 
2420  NSMutableDictionary* field2 = self.mutablePasswordTemplateCopy;
2421  [field2 setValue:@{
2422  @"uniqueIdentifier" : @"field2",
2423  @"hints" : @[ @"hint2" ],
2424  @"editingValue" : @{@"text" : @""}
2425  }
2426  forKey:@"autofill"];
2427 
2428  NSMutableDictionary* config = [field1 mutableCopy];
2429  [config setValue:@[ field1, field2 ] forKey:@"fields"];
2430 
2431  [self setClientId:123 configuration:config];
2432  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2433 
2434  // Find all the FlutterTextInputViews we created.
2435  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
2436 
2437  // Both fields are installed and visible because it's a password group.
2438  XCTAssertEqual(inputFields.count, 2ul);
2439  XCTAssertEqual(self.viewsVisibleToAutofill.count, 2ul);
2440 
2441  // Find the inactive autofillable input field.
2442  FlutterTextInputView* inactiveView = inputFields[1];
2443  [inactiveView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]
2444  withText:@"Autofilled!"];
2445  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2446 
2447  // Verify behavior.
2448  OCMVerify([engine flutterTextInputView:inactiveView
2449  updateEditingClient:0
2450  withState:[OCMArg isNotNil]
2451  withTag:@"field2"]);
2452 }
2453 
2454 - (void)testPasswordAutofillHack {
2455  NSDictionary* config = self.mutableTemplateCopy;
2456  [config setValue:@"YES" forKey:@"obscureText"];
2457  [self setClientId:123 configuration:config];
2458 
2459  // Find all the FlutterTextInputViews we created.
2460  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
2461 
2462  FlutterTextInputView* inputView = inputFields[0];
2463 
2464  XCTAssert([inputView isKindOfClass:[UITextField class]]);
2465  // FlutterSecureTextInputView does not respond to font,
2466  // but it should return the default UITextField.font.
2467  XCTAssertNotEqual([inputView performSelector:@selector(font)], nil);
2468 }
2469 
2470 - (void)testClearAutofillContextClearsSelection {
2471  NSMutableDictionary* regularField = self.mutableTemplateCopy;
2472  NSDictionary* editingValue = @{
2473  @"text" : @"REGULAR_TEXT_FIELD",
2474  @"composingBase" : @0,
2475  @"composingExtent" : @3,
2476  @"selectionBase" : @1,
2477  @"selectionExtent" : @4
2478  };
2479  [regularField setValue:@{
2480  @"uniqueIdentifier" : @"field2",
2481  @"hints" : @[ @"hint2" ],
2482  @"editingValue" : editingValue,
2483  }
2484  forKey:@"autofill"];
2485  [regularField addEntriesFromDictionary:editingValue];
2486  [self setClientId:123 configuration:regularField];
2487  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2488  XCTAssertEqual(self.installedInputViews.count, 1ul);
2489 
2490  FlutterTextInputView* oldInputView = self.installedInputViews[0];
2491  XCTAssert([oldInputView.text isEqualToString:@"REGULAR_TEXT_FIELD"]);
2492  FlutterTextRange* selectionRange = (FlutterTextRange*)oldInputView.selectedTextRange;
2493  XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(1, 3)));
2494 
2495  // Replace the original password field with new one. This should remove
2496  // the old password field, but not immediately.
2497  [self setClientId:124 configuration:self.mutablePasswordTemplateCopy];
2498  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2499 
2500  XCTAssertEqual(self.installedInputViews.count, 2ul);
2501 
2502  [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO];
2503  XCTAssertEqual(self.installedInputViews.count, 1ul);
2504 
2505  // Verify the old input view is properly cleaned up.
2506  XCTAssert([oldInputView.text isEqualToString:@""]);
2507  selectionRange = (FlutterTextRange*)oldInputView.selectedTextRange;
2508  XCTAssert(NSEqualRanges(selectionRange.range, NSMakeRange(0, 0)));
2509 }
2510 
2511 - (void)testGarbageInputViewsAreNotRemovedImmediately {
2512  // Add a password field that should autofill.
2513  [self setClientId:123 configuration:self.mutablePasswordTemplateCopy];
2514  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2515 
2516  XCTAssertEqual(self.installedInputViews.count, 1ul);
2517  // Add an input field that doesn't autofill. This should remove the password
2518  // field, but not immediately.
2519  [self setClientId:124 configuration:self.mutableTemplateCopy];
2520  [self ensureOnlyActiveViewCanBecomeFirstResponder];
2521 
2522  XCTAssertEqual(self.installedInputViews.count, 2ul);
2523 
2524  [self commitAutofillContextAndVerify];
2525 }
2526 
2527 - (void)testScribbleSetSelectionRects {
2528  NSMutableDictionary* regularField = self.mutableTemplateCopy;
2529  NSDictionary* editingValue = @{
2530  @"text" : @"REGULAR_TEXT_FIELD",
2531  @"composingBase" : @0,
2532  @"composingExtent" : @3,
2533  @"selectionBase" : @1,
2534  @"selectionExtent" : @4
2535  };
2536  [regularField setValue:@{
2537  @"uniqueIdentifier" : @"field1",
2538  @"hints" : @[ @"hint2" ],
2539  @"editingValue" : editingValue,
2540  }
2541  forKey:@"autofill"];
2542  [regularField addEntriesFromDictionary:editingValue];
2543  [self setClientId:123 configuration:regularField];
2544  XCTAssertEqual(self.installedInputViews.count, 1ul);
2545  XCTAssertEqual([textInputPlugin.activeView.selectionRects count], 0u);
2546 
2547  NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
2548  NSArray* selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
2549  FlutterMethodCall* methodCall =
2550  [FlutterMethodCall methodCallWithMethodName:@"Scribble.setSelectionRects"
2551  arguments:selectionRects];
2552  [textInputPlugin handleMethodCall:methodCall
2553  result:^(id _Nullable result){
2554  }];
2555 
2556  XCTAssertEqual([textInputPlugin.activeView.selectionRects count], 1u);
2557 }
2558 
2559 - (void)testDecommissionedViewAreNotReusedByAutofill {
2560  // Regression test for https://github.com/flutter/flutter/issues/84407.
2561  NSMutableDictionary* configuration = self.mutableTemplateCopy;
2562  [configuration setValue:@{
2563  @"uniqueIdentifier" : @"field1",
2564  @"hints" : @[ UITextContentTypePassword ],
2565  @"editingValue" : @{@"text" : @""}
2566  }
2567  forKey:@"autofill"];
2568  [configuration setValue:@[ [configuration copy] ] forKey:@"fields"];
2569 
2570  [self setClientId:123 configuration:configuration];
2571 
2572  [self setTextInputHide];
2573  UIView* previousActiveView = textInputPlugin.activeView;
2574 
2575  [self setClientId:124 configuration:configuration];
2576 
2577  // Make sure the autofillable view is reused.
2578  XCTAssertEqual(previousActiveView, textInputPlugin.activeView);
2579  XCTAssertNotNil(previousActiveView);
2580  // Does not crash.
2581 }
2582 
2583 - (void)testInitialActiveViewCantAccessTextInputDelegate {
2584  // Before the framework sends the first text input configuration,
2585  // the dummy "activeView" we use should never have access to
2586  // its textInputDelegate.
2587  XCTAssertNil(textInputPlugin.activeView.textInputDelegate);
2588 }
2589 
2590 #pragma mark - Accessibility - Tests
2591 
2592 - (void)testUITextInputAccessibilityNotHiddenWhenShowed {
2593  [self setClientId:123 configuration:self.mutableTemplateCopy];
2594 
2595  // Send show text input method call.
2596  [self setTextInputShow];
2597  // Find all the FlutterTextInputViews we created.
2598  NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
2599 
2600  // The input view should not be hidden.
2601  XCTAssertEqual([inputFields count], 1u);
2602 
2603  // Send hide text input method call.
2604  [self setTextInputHide];
2605 
2606  inputFields = self.installedInputViews;
2607 
2608  // The input view should be hidden.
2609  XCTAssertEqual([inputFields count], 0u);
2610 }
2611 
2612 - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
2613  FlutterTextInputViewSpy* inputView =
2614  [[FlutterTextInputViewSpy alloc] initWithOwner:textInputPlugin];
2615  UIView* container = [[UIView alloc] init];
2616  UIAccessibilityElement* backing =
2617  [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
2618  inputView.backingTextInputAccessibilityObject = backing;
2619  // Simulate accessibility focus.
2620  inputView.isAccessibilityFocused = YES;
2621  [inputView accessibilityElementDidBecomeFocused];
2622 
2623  XCTAssertEqual(inputView.receivedNotification, UIAccessibilityScreenChangedNotification);
2624  XCTAssertEqual(inputView.receivedNotificationTarget, backing);
2625 }
2626 
2627 - (void)testFlutterTokenizerCanParseLines {
2628  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2629  id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
2630 
2631  // The tokenizer returns zero range When text is empty.
2632  FlutterTextRange* range = [self getLineRangeFromTokenizer:tokenizer atIndex:0];
2633  XCTAssertEqual(range.range.location, 0u);
2634  XCTAssertEqual(range.range.length, 0u);
2635 
2636  [inputView insertText:@"how are you\nI am fine, Thank you"];
2637 
2638  range = [self getLineRangeFromTokenizer:tokenizer atIndex:0];
2639  XCTAssertEqual(range.range.location, 0u);
2640  XCTAssertEqual(range.range.length, 11u);
2641 
2642  range = [self getLineRangeFromTokenizer:tokenizer atIndex:2];
2643  XCTAssertEqual(range.range.location, 0u);
2644  XCTAssertEqual(range.range.length, 11u);
2645 
2646  range = [self getLineRangeFromTokenizer:tokenizer atIndex:11];
2647  XCTAssertEqual(range.range.location, 0u);
2648  XCTAssertEqual(range.range.length, 11u);
2649 
2650  range = [self getLineRangeFromTokenizer:tokenizer atIndex:12];
2651  XCTAssertEqual(range.range.location, 12u);
2652  XCTAssertEqual(range.range.length, 20u);
2653 
2654  range = [self getLineRangeFromTokenizer:tokenizer atIndex:15];
2655  XCTAssertEqual(range.range.location, 12u);
2656  XCTAssertEqual(range.range.length, 20u);
2657 
2658  range = [self getLineRangeFromTokenizer:tokenizer atIndex:32];
2659  XCTAssertEqual(range.range.location, 12u);
2660  XCTAssertEqual(range.range.length, 20u);
2661 }
2662 
2663 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
2664  FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
2665  FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
2666  myInputPlugin.viewController = flutterViewController;
2667 
2668  __weak UIView* activeView;
2669  @autoreleasepool {
2670  FlutterMethodCall* setClientCall = [FlutterMethodCall
2671  methodCallWithMethodName:@"TextInput.setClient"
2672  arguments:@[
2673  [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy
2674  ]];
2675  [myInputPlugin handleMethodCall:setClientCall
2676  result:^(id _Nullable result){
2677  }];
2678  activeView = myInputPlugin.textInputView;
2679  FlutterMethodCall* hideCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.hide"
2680  arguments:@[]];
2681  [myInputPlugin handleMethodCall:hideCall
2682  result:^(id _Nullable result){
2683  }];
2684  XCTAssertNotNil(activeView);
2685  }
2686  // This assert proves the myInputPlugin.textInputView is not deallocated.
2687  XCTAssertNotNil(activeView);
2688 }
2689 
2690 - (void)testFlutterTextInputPluginHostViewNilCrash {
2691  FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
2692  myInputPlugin.viewController = nil;
2693  XCTAssertThrows([myInputPlugin hostView], @"Throws exception if host view is nil");
2694 }
2695 
2696 - (void)testFlutterTextInputPluginHostViewNotNil {
2697  FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
2698  FlutterEngine* flutterEngine = [[FlutterEngine alloc] init];
2699  [flutterEngine runWithEntrypoint:nil];
2700  flutterEngine.viewController = flutterViewController;
2701  XCTAssertNotNil(flutterEngine.textInputPlugin.viewController);
2702  XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
2703 }
2704 
2705 - (void)testSetPlatformViewClient {
2706  FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
2707  FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
2708  myInputPlugin.viewController = flutterViewController;
2709 
2710  FlutterMethodCall* setClientCall = [FlutterMethodCall
2711  methodCallWithMethodName:@"TextInput.setClient"
2712  arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
2713  [myInputPlugin handleMethodCall:setClientCall
2714  result:^(id _Nullable result){
2715  }];
2716  UIView* activeView = myInputPlugin.textInputView;
2717  XCTAssertNotNil(activeView.superview, @"activeView must be added to the view hierarchy.");
2718  FlutterMethodCall* setPlatformViewClientCall = [FlutterMethodCall
2719  methodCallWithMethodName:@"TextInput.setPlatformViewClient"
2720  arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
2721  [myInputPlugin handleMethodCall:setPlatformViewClientCall
2722  result:^(id _Nullable result){
2723  }];
2724  XCTAssertNil(activeView.superview, @"activeView must be removed from view hierarchy.");
2725 }
2726 
2727 - (void)testInteractiveKeyboardAfterUserScrollWillResignFirstResponder {
2728  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2729  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2730 
2731  [inputView setTextInputClient:123];
2732  [inputView reloadInputViews];
2733  [inputView becomeFirstResponder];
2734  XCTAssert(inputView.isFirstResponder);
2735 
2736  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2737  [NSNotificationCenter.defaultCenter
2738  postNotificationName:UIKeyboardWillShowNotification
2739  object:nil
2740  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2741  FlutterMethodCall* onPointerMoveCall =
2742  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2743  arguments:@{@"pointerY" : @(500)}];
2744  [textInputPlugin handleMethodCall:onPointerMoveCall
2745  result:^(id _Nullable result){
2746  }];
2747  XCTAssertFalse(inputView.isFirstResponder);
2748  textInputPlugin.cachedFirstResponder = nil;
2749 }
2750 
2751 - (void)testInteractiveKeyboardAfterUserScrollToTopOfKeyboardWillTakeScreenshot {
2752  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2753  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
2754  UIScene* scene = scenes.anyObject;
2755  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
2756  UIWindowScene* windowScene = (UIWindowScene*)scene;
2757  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
2758  UIWindow* window = windowScene.windows[0];
2759  [window addSubview:viewController.view];
2760 
2761  [viewController loadView];
2762 
2763  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2764  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2765 
2766  [inputView setTextInputClient:123];
2767  [inputView reloadInputViews];
2768  [inputView becomeFirstResponder];
2769 
2770  if (textInputPlugin.keyboardView.superview != nil) {
2771  for (UIView* subView in textInputPlugin.keyboardViewContainer.subviews) {
2772  [subView removeFromSuperview];
2773  }
2774  }
2775  XCTAssert(textInputPlugin.keyboardView.superview == nil);
2776  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2777  [NSNotificationCenter.defaultCenter
2778  postNotificationName:UIKeyboardWillShowNotification
2779  object:nil
2780  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2781  FlutterMethodCall* onPointerMoveCall =
2782  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2783  arguments:@{@"pointerY" : @(510)}];
2784  [textInputPlugin handleMethodCall:onPointerMoveCall
2785  result:^(id _Nullable result){
2786  }];
2787  XCTAssertFalse(textInputPlugin.keyboardView.superview == nil);
2788  for (UIView* subView in textInputPlugin.keyboardViewContainer.subviews) {
2789  [subView removeFromSuperview];
2790  }
2791  textInputPlugin.cachedFirstResponder = nil;
2792 }
2793 
2794 - (void)testInteractiveKeyboardScreenshotWillBeMovedDownAfterUserScroll {
2795  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2796  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
2797  UIScene* scene = scenes.anyObject;
2798  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
2799  UIWindowScene* windowScene = (UIWindowScene*)scene;
2800  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
2801  UIWindow* window = windowScene.windows[0];
2802  [window addSubview:viewController.view];
2803 
2804  [viewController loadView];
2805 
2806  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2807  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2808 
2809  [inputView setTextInputClient:123];
2810  [inputView reloadInputViews];
2811  [inputView becomeFirstResponder];
2812 
2813  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2814  [NSNotificationCenter.defaultCenter
2815  postNotificationName:UIKeyboardWillShowNotification
2816  object:nil
2817  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2818  FlutterMethodCall* onPointerMoveCall =
2819  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2820  arguments:@{@"pointerY" : @(510)}];
2821  [textInputPlugin handleMethodCall:onPointerMoveCall
2822  result:^(id _Nullable result){
2823  }];
2824  XCTAssert(textInputPlugin.keyboardView.superview != nil);
2825 
2826  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2827 
2828  FlutterMethodCall* onPointerMoveCallMove =
2829  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2830  arguments:@{@"pointerY" : @(600)}];
2831  [textInputPlugin handleMethodCall:onPointerMoveCallMove
2832  result:^(id _Nullable result){
2833  }];
2834  XCTAssert(textInputPlugin.keyboardView.superview != nil);
2835 
2836  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2837 
2838  for (UIView* subView in textInputPlugin.keyboardViewContainer.subviews) {
2839  [subView removeFromSuperview];
2840  }
2841  textInputPlugin.cachedFirstResponder = nil;
2842 }
2843 
2844 - (void)testInteractiveKeyboardScreenshotWillBeMovedToOrginalPositionAfterUserScroll {
2845  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2846  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
2847  UIScene* scene = scenes.anyObject;
2848  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
2849  UIWindowScene* windowScene = (UIWindowScene*)scene;
2850  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
2851  UIWindow* window = windowScene.windows[0];
2852  [window addSubview:viewController.view];
2853 
2854  [viewController loadView];
2855 
2856  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2857  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2858 
2859  [inputView setTextInputClient:123];
2860  [inputView reloadInputViews];
2861  [inputView becomeFirstResponder];
2862 
2863  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2864  [NSNotificationCenter.defaultCenter
2865  postNotificationName:UIKeyboardWillShowNotification
2866  object:nil
2867  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2868  FlutterMethodCall* onPointerMoveCall =
2869  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2870  arguments:@{@"pointerY" : @(500)}];
2871  [textInputPlugin handleMethodCall:onPointerMoveCall
2872  result:^(id _Nullable result){
2873  }];
2874  XCTAssert(textInputPlugin.keyboardView.superview != nil);
2875  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2876 
2877  FlutterMethodCall* onPointerMoveCallMove =
2878  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2879  arguments:@{@"pointerY" : @(600)}];
2880  [textInputPlugin handleMethodCall:onPointerMoveCallMove
2881  result:^(id _Nullable result){
2882  }];
2883  XCTAssert(textInputPlugin.keyboardView.superview != nil);
2884  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y, 600.0);
2885 
2886  FlutterMethodCall* onPointerMoveCallBackUp =
2887  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2888  arguments:@{@"pointerY" : @(10)}];
2889  [textInputPlugin handleMethodCall:onPointerMoveCallBackUp
2890  result:^(id _Nullable result){
2891  }];
2892  XCTAssert(textInputPlugin.keyboardView.superview != nil);
2893  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y, keyboardFrame.origin.y);
2894  for (UIView* subView in textInputPlugin.keyboardViewContainer.subviews) {
2895  [subView removeFromSuperview];
2896  }
2897  textInputPlugin.cachedFirstResponder = nil;
2898 }
2899 
2900 - (void)testInteractiveKeyboardFindFirstResponderRecursive {
2901  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2902  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2903  [inputView setTextInputClient:123];
2904  [inputView reloadInputViews];
2905  [inputView becomeFirstResponder];
2906 
2907  UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2908  XCTAssertEqualObjects(inputView, firstResponder);
2909  textInputPlugin.cachedFirstResponder = nil;
2910 }
2911 
2912 - (void)testInteractiveKeyboardFindFirstResponderRecursiveInMultipleSubviews {
2913  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2914  FlutterTextInputView* subInputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2915  FlutterTextInputView* otherSubInputView =
2916  [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2917  FlutterTextInputView* subFirstResponderInputView =
2918  [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2919  [subInputView addSubview:subFirstResponderInputView];
2920  [inputView addSubview:subInputView];
2921  [inputView addSubview:otherSubInputView];
2922  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2923  [inputView setTextInputClient:123];
2924  [inputView reloadInputViews];
2925  [subInputView setTextInputClient:123];
2926  [subInputView reloadInputViews];
2927  [otherSubInputView setTextInputClient:123];
2928  [otherSubInputView reloadInputViews];
2929  [subFirstResponderInputView setTextInputClient:123];
2930  [subFirstResponderInputView reloadInputViews];
2931  [subFirstResponderInputView becomeFirstResponder];
2932 
2933  UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2934  XCTAssertEqualObjects(subFirstResponderInputView, firstResponder);
2935  textInputPlugin.cachedFirstResponder = nil;
2936 }
2937 
2938 - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
2939  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
2940  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
2941  [inputView setTextInputClient:123];
2942  [inputView reloadInputViews];
2943 
2944  UIView* firstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2945  XCTAssertNil(firstResponder);
2946  textInputPlugin.cachedFirstResponder = nil;
2947 }
2948 
2949 - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
2950  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2951  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
2952  UIScene* scene = scenes.anyObject;
2953  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
2954  UIWindowScene* windowScene = (UIWindowScene*)scene;
2955  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
2956  UIWindow* window = windowScene.windows[0];
2957  [window addSubview:viewController.view];
2958 
2959  [viewController loadView];
2960 
2961  XCTestExpectation* expectation = [[XCTestExpectation alloc]
2962  initWithDescription:
2963  @"didResignFirstResponder is called after screenshot keyboard dismissed."];
2964  OCMStub([engine flutterTextInputView:[OCMArg any] didResignFirstResponderWithTextInputClient:0])
2965  .andDo(^(NSInvocation* invocation) {
2966  [expectation fulfill];
2967  });
2968  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
2969  [NSNotificationCenter.defaultCenter
2970  postNotificationName:UIKeyboardWillShowNotification
2971  object:nil
2972  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
2973  FlutterMethodCall* initialMoveCall =
2974  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2975  arguments:@{@"pointerY" : @(500)}];
2976  [textInputPlugin handleMethodCall:initialMoveCall
2977  result:^(id _Nullable result){
2978  }];
2979  FlutterMethodCall* subsequentMoveCall =
2980  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2981  arguments:@{@"pointerY" : @(1000)}];
2982  [textInputPlugin handleMethodCall:subsequentMoveCall
2983  result:^(id _Nullable result){
2984  }];
2985 
2986  FlutterMethodCall* pointerUpCall =
2987  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"
2988  arguments:@{@"pointerY" : @(1000)}];
2989  [textInputPlugin handleMethodCall:pointerUpCall
2990  result:^(id _Nullable result){
2991  }];
2992 
2993  [self waitForExpectations:@[ expectation ] timeout:2.0];
2994  textInputPlugin.cachedFirstResponder = nil;
2995 }
2996 
2997 - (void)testInteractiveKeyboardScreenshotDismissedAfterPointerLiftedAboveMiddleYOfKeyboard {
2998  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2999  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
3000  UIScene* scene = scenes.anyObject;
3001  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
3002  UIWindowScene* windowScene = (UIWindowScene*)scene;
3003  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
3004  UIWindow* window = windowScene.windows[0];
3005  [window addSubview:viewController.view];
3006 
3007  [viewController loadView];
3008 
3009  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3010  [NSNotificationCenter.defaultCenter
3011  postNotificationName:UIKeyboardWillShowNotification
3012  object:nil
3013  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3014  FlutterMethodCall* initialMoveCall =
3015  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3016  arguments:@{@"pointerY" : @(500)}];
3017  [textInputPlugin handleMethodCall:initialMoveCall
3018  result:^(id _Nullable result){
3019  }];
3020  FlutterMethodCall* subsequentMoveCall =
3021  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3022  arguments:@{@"pointerY" : @(1000)}];
3023  [textInputPlugin handleMethodCall:subsequentMoveCall
3024  result:^(id _Nullable result){
3025  }];
3026 
3027  FlutterMethodCall* subsequentMoveBackUpCall =
3028  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3029  arguments:@{@"pointerY" : @(0)}];
3030  [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3031  result:^(id _Nullable result){
3032  }];
3033 
3034  FlutterMethodCall* pointerUpCall =
3035  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"
3036  arguments:@{@"pointerY" : @(0)}];
3037  [textInputPlugin handleMethodCall:pointerUpCall
3038  result:^(id _Nullable result){
3039  }];
3040  NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3041  return textInputPlugin.keyboardViewContainer.subviews.count == 0;
3042  }];
3043  XCTNSPredicateExpectation* expectation =
3044  [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3045  [self waitForExpectations:@[ expectation ] timeout:10.0];
3046  textInputPlugin.cachedFirstResponder = nil;
3047 }
3048 
3049 - (void)testInteractiveKeyboardKeyboardReappearsAfterPointerLiftedAboveMiddleYOfKeyboard {
3050  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3051  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
3052  UIScene* scene = scenes.anyObject;
3053  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
3054  UIWindowScene* windowScene = (UIWindowScene*)scene;
3055  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
3056  UIWindow* window = windowScene.windows[0];
3057  [window addSubview:viewController.view];
3058 
3059  [viewController loadView];
3060 
3061  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
3062  [UIApplication.sharedApplication.keyWindow addSubview:inputView];
3063 
3064  [inputView setTextInputClient:123];
3065  [inputView reloadInputViews];
3066  [inputView becomeFirstResponder];
3067 
3068  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3069  [NSNotificationCenter.defaultCenter
3070  postNotificationName:UIKeyboardWillShowNotification
3071  object:nil
3072  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3073  FlutterMethodCall* initialMoveCall =
3074  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3075  arguments:@{@"pointerY" : @(500)}];
3076  [textInputPlugin handleMethodCall:initialMoveCall
3077  result:^(id _Nullable result){
3078  }];
3079  FlutterMethodCall* subsequentMoveCall =
3080  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3081  arguments:@{@"pointerY" : @(1000)}];
3082  [textInputPlugin handleMethodCall:subsequentMoveCall
3083  result:^(id _Nullable result){
3084  }];
3085 
3086  FlutterMethodCall* subsequentMoveBackUpCall =
3087  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3088  arguments:@{@"pointerY" : @(0)}];
3089  [textInputPlugin handleMethodCall:subsequentMoveBackUpCall
3090  result:^(id _Nullable result){
3091  }];
3092 
3093  FlutterMethodCall* pointerUpCall =
3094  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"
3095  arguments:@{@"pointerY" : @(0)}];
3096  [textInputPlugin handleMethodCall:pointerUpCall
3097  result:^(id _Nullable result){
3098  }];
3099  NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3100  return textInputPlugin.cachedFirstResponder.isFirstResponder;
3101  }];
3102  XCTNSPredicateExpectation* expectation =
3103  [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3104  [self waitForExpectations:@[ expectation ] timeout:10.0];
3105  textInputPlugin.cachedFirstResponder = nil;
3106 }
3107 
3108 - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
3109  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3110  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
3111  UIScene* scene = scenes.anyObject;
3112  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
3113  UIWindowScene* windowScene = (UIWindowScene*)scene;
3114  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
3115  UIWindow* window = windowScene.windows[0];
3116  [window addSubview:viewController.view];
3117 
3118  [viewController loadView];
3119 
3120  XCTestExpectation* expectation =
3121  [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3122  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3123  [NSNotificationCenter.defaultCenter
3124  postNotificationName:UIKeyboardWillShowNotification
3125  object:nil
3126  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3127  FlutterMethodCall* initialMoveCall =
3128  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3129  arguments:@{@"pointerY" : @(500)}];
3130  [textInputPlugin handleMethodCall:initialMoveCall
3131  result:^(id _Nullable result){
3132  }];
3133  FlutterMethodCall* subsequentMoveCall =
3134  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3135  arguments:@{@"pointerY" : @(1000)}];
3136  [textInputPlugin handleMethodCall:subsequentMoveCall
3137  result:^(id _Nullable result){
3138  }];
3139  FlutterMethodCall* upwardVelocityMoveCall =
3140  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3141  arguments:@{@"pointerY" : @(500)}];
3142  [textInputPlugin handleMethodCall:upwardVelocityMoveCall
3143  result:^(id _Nullable result){
3144  }];
3145 
3146  FlutterMethodCall* pointerUpCall =
3147  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"
3148  arguments:@{@"pointerY" : @(0)}];
3149  [textInputPlugin
3150  handleMethodCall:pointerUpCall
3151  result:^(id _Nullable result) {
3152  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3153  viewController.flutterScreenIfViewLoaded.bounds.size.height -
3154  keyboardFrame.origin.y);
3155  [expectation fulfill];
3156  }];
3157  textInputPlugin.cachedFirstResponder = nil;
3158 }
3159 
3160 - (void)testInteractiveKeyboardKeyboardAnimatesToDismissalPositionalOnPointerUp {
3161  NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
3162  XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
3163  UIScene* scene = scenes.anyObject;
3164  XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
3165  UIWindowScene* windowScene = (UIWindowScene*)scene;
3166  XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
3167  UIWindow* window = windowScene.windows[0];
3168  [window addSubview:viewController.view];
3169 
3170  [viewController loadView];
3171 
3172  XCTestExpectation* expectation =
3173  [[XCTestExpectation alloc] initWithDescription:@"Keyboard animates to proper position."];
3174  CGRect keyboardFrame = CGRectMake(0, 500, 500, 500);
3175  [NSNotificationCenter.defaultCenter
3176  postNotificationName:UIKeyboardWillShowNotification
3177  object:nil
3178  userInfo:@{UIKeyboardFrameEndUserInfoKey : @(keyboardFrame)}];
3179  FlutterMethodCall* initialMoveCall =
3180  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3181  arguments:@{@"pointerY" : @(500)}];
3182  [textInputPlugin handleMethodCall:initialMoveCall
3183  result:^(id _Nullable result){
3184  }];
3185  FlutterMethodCall* subsequentMoveCall =
3186  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
3187  arguments:@{@"pointerY" : @(1000)}];
3188  [textInputPlugin handleMethodCall:subsequentMoveCall
3189  result:^(id _Nullable result){
3190  }];
3191 
3192  FlutterMethodCall* pointerUpCall =
3193  [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"
3194  arguments:@{@"pointerY" : @(1000)}];
3195  [textInputPlugin
3196  handleMethodCall:pointerUpCall
3197  result:^(id _Nullable result) {
3198  XCTAssertEqual(textInputPlugin.keyboardViewContainer.frame.origin.y,
3199  viewController.flutterScreenIfViewLoaded.bounds.size.height);
3200  [expectation fulfill];
3201  }];
3202  textInputPlugin.cachedFirstResponder = nil;
3203 }
3204 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
3205  [UIView setAnimationsEnabled:YES];
3206  [textInputPlugin showKeyboardAndRemoveScreenshot];
3207  XCTAssertFalse(
3208  UIView.areAnimationsEnabled,
3209  @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
3210 }
3211 
3212 - (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
3213  [UIView setAnimationsEnabled:YES];
3214  [textInputPlugin showKeyboardAndRemoveScreenshot];
3215 
3216  NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
3217  // This will be enabled after a delay
3218  return UIView.areAnimationsEnabled;
3219  }];
3220  XCTNSPredicateExpectation* expectation =
3221  [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
3222  [self waitForExpectations:@[ expectation ] timeout:10.0];
3223 }
3224 
3225 @end
FLUTTER_ASSERT_ARC
#define FLUTTER_ASSERT_ARC
Definition: FlutterMacros.h:44
FlutterTextInputViewSpy
Definition: FlutterTextInputPluginTest.mm:35
+[FlutterTextPosition positionWithIndex:]
instancetype positionWithIndex:(NSUInteger index)
Definition: FlutterTextInputPlugin.mm:519
selectionRects
NSArray< FlutterTextSelectionRect * > * selectionRects
Definition: FlutterTextInputPlugin.h:157
FlutterEngine
Definition: FlutterEngine.h:59
FlutterSecureTextInputView::textField
UITextField * textField
Definition: FlutterTextInputPlugin.mm:732
FlutterTextInputDelegate-p
Definition: FlutterTextInputDelegate.h:33
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterViewController
Definition: FlutterViewController.h:55
FlutterEngine.h
isScribbleAvailable
BOOL isScribbleAvailable
Definition: FlutterTextInputPlugin.h:159
-[FlutterEngine runWithEntrypoint:]
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
FlutterEngine_Test.h
-[FlutterEngine flutterTextInputView:performAction:withClient:]
void flutterTextInputView:performAction:withClient:(FlutterTextInputView *textInputView,[performAction] FlutterTextInputAction action,[withClient] int client)
FlutterTextInputPlugin.h
FlutterEngine::viewController
FlutterViewController * viewController
Definition: FlutterEngine.h:325
FlutterTextSelectionRect::rect
CGRect rect
Definition: FlutterTextInputPlugin.h:89
FlutterTextRange
Definition: FlutterTextInputPlugin.h:75
FlutterMacros.h
-[FlutterEngine setBinaryMessenger:]
void setBinaryMessenger:(FlutterBinaryMessengerRelay *binaryMessenger)
FlutterTextInputViewSpy::isAccessibilityFocused
BOOL isAccessibilityFocused
Definition: FlutterTextInputPluginTest.mm:38
-[FlutterTextInputPlugin handleMethodCall:result:]
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
Definition: FlutterTextInputPlugin.mm:2327
-[FlutterTextInputPlugin textInputView]
UIView< UITextInput > * textInputView()
Definition: FlutterTextInputPlugin.mm:2323
kInvalidFirstRect
const CGRect kInvalidFirstRect
Definition: FlutterTextInputPlugin.mm:35
viewController
FlutterViewController * viewController
Definition: FlutterTextInputPluginTest.mm:92
+[FlutterTextRange rangeWithNSRange:]
instancetype rangeWithNSRange:(NSRange range)
Definition: FlutterTextInputPlugin.mm:542
FlutterSecureTextInputView
Definition: FlutterTextInputPlugin.mm:731
FlutterTextInputView
Definition: FlutterTextInputPlugin.mm:787
FlutterTextInputViewSpy::receivedNotification
UIAccessibilityNotifications receivedNotification
Definition: FlutterTextInputPluginTest.mm:36
FlutterBinaryMessengerRelay.h
FlutterMethodCall
Definition: FlutterCodecs.h:220
+[FlutterTextSelectionRect selectionRectWithRect:position:]
instancetype selectionRectWithRect:position:(CGRect rect,[position] NSUInteger position)
Definition: FlutterTextInputPlugin.mm:668
FlutterTextRange::range
NSRange range
Definition: FlutterTextInputPlugin.h:77
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:29
FlutterTextPosition::affinity
UITextStorageDirection affinity
Definition: FlutterTextInputPlugin.h:66
UIViewController+FlutterScreenAndSceneIfLoaded.h
FlutterTextInputPluginTest
Definition: FlutterTextInputPluginTest.mm:83
FlutterTextSelectionRect::position
NSUInteger position
Definition: FlutterTextInputPlugin.h:90
_passwordTemplate
NSDictionary * _passwordTemplate
Definition: FlutterTextInputPluginTest.mm:86
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
textInputPlugin
FlutterTextInputPlugin * textInputPlugin
Definition: FlutterTextInputPluginTest.mm:90
FlutterTextPosition
Definition: FlutterTextInputPlugin.h:63
FlutterBinaryMessengerRelay
Definition: FlutterBinaryMessengerRelay.h:14
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:453
FlutterTextInputPlugin::viewController
UIIndirectScribbleInteractionDelegate UIViewController * viewController
Definition: FlutterTextInputPlugin.h:32
FlutterTextPosition::index
NSUInteger index
Definition: FlutterTextInputPlugin.h:65
-[FlutterEngine runWithEntrypoint:initialRoute:]
BOOL runWithEntrypoint:initialRoute:(nullable NSString *entrypoint,[initialRoute] nullable NSString *initialRoute)
FlutterTextSelectionRect
Definition: FlutterTextInputPlugin.h:87
id
int32_t id
Definition: SemanticsObjectTestMocks.h:20
+[FlutterTextSelectionRect selectionRectWithRect:position:writingDirection:]
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)
Definition: FlutterTextInputPlugin.mm:677
FlutterTextInputViewSpy::receivedNotificationTarget
id receivedNotificationTarget
Definition: FlutterTextInputPluginTest.mm:37
FlutterViewController.h