@@ -27,6 +27,9 @@ @interface FlutterTextInputPlugin ()
27
27
@property (nonatomic , assign ) FlutterTextInputView* activeView;
28
28
@property (nonatomic , readonly )
29
29
NSMutableDictionary <NSString*, FlutterTextInputView*>* autofillContext;
30
+
31
+ - (void )collectGarbageInputViews ;
32
+ - (UIView*)textInputParentView ;
30
33
@end
31
34
32
35
@interface FlutterTextInputPluginTest : XCTestCase
@@ -81,12 +84,7 @@ - (NSMutableDictionary*)mutableTemplateCopy {
81
84
}
82
85
83
86
- (NSArray <FlutterTextInputView*>*)installedInputViews {
84
- UIWindow* keyWindow =
85
- [[[UIApplication sharedApplication ] windows ]
86
- filteredArrayUsingPredicate: [NSPredicate predicateWithFormat: @" isKeyWindow == YES " ]]
87
- .firstObject ;
88
-
89
- return [keyWindow.subviews
87
+ return [textInputPlugin.textInputParentView.subviews
90
88
filteredArrayUsingPredicate: [NSPredicate predicateWithFormat: @" self isKindOfClass: %@" ,
91
89
[FlutterTextInputView class ]]];
92
90
}
@@ -407,6 +405,12 @@ - (void)commitAutofillContextAndVerify {
407
405
XCTAssertEqual (textInputPlugin.autofillContext .count , 0 );
408
406
}
409
407
408
+ - (void )ensureOnlyActiveViewCanBecomeFirstResponder {
409
+ for (FlutterTextInputView* inputView in self.installedInputViews ) {
410
+ XCTAssertEqual (inputView.canBecomeFirstResponder , inputView == textInputPlugin.activeView );
411
+ }
412
+ }
413
+
410
414
#pragma mark - Autofill - Tests
411
415
412
416
- (void )testAutofillContext {
@@ -434,8 +438,11 @@ - (void)testAutofillContext {
434
438
XCTAssertEqual (self.viewsVisibleToAutofill .count , 2 );
435
439
436
440
XCTAssertEqual (textInputPlugin.autofillContext .count , 2 );
441
+
442
+ [textInputPlugin collectGarbageInputViews ];
437
443
XCTAssertEqual (self.installedInputViews .count , 2 );
438
444
XCTAssertEqual (textInputPlugin.textInputView , textInputPlugin.autofillContext [@" field1" ]);
445
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
439
446
440
447
// The configuration changes.
441
448
NSMutableDictionary * field3 = self.mutablePasswordTemplateCopy ;
@@ -454,8 +461,11 @@ - (void)testAutofillContext {
454
461
455
462
XCTAssertEqual (self.viewsVisibleToAutofill .count , 2 );
456
463
XCTAssertEqual (textInputPlugin.autofillContext .count , 3 );
464
+
465
+ [textInputPlugin collectGarbageInputViews ];
457
466
XCTAssertEqual (self.installedInputViews .count , 3 );
458
467
XCTAssertEqual (textInputPlugin.textInputView , textInputPlugin.autofillContext [@" field1" ]);
468
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
459
469
460
470
// Old autofill input fields are still installed and reused.
461
471
for (NSString * key in oldContext.allKeys ) {
@@ -467,9 +477,12 @@ - (void)testAutofillContext {
467
477
468
478
oldContext = textInputPlugin.autofillContext ;
469
479
[self setClientId: 124 configuration: config];
480
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
470
481
471
482
XCTAssertEqual (self.viewsVisibleToAutofill .count , 1 );
472
483
XCTAssertEqual (textInputPlugin.autofillContext .count , 3 );
484
+
485
+ [textInputPlugin collectGarbageInputViews ];
473
486
XCTAssertEqual (self.installedInputViews .count , 4 );
474
487
475
488
// Old autofill input fields are still installed and reused.
@@ -478,6 +491,7 @@ - (void)testAutofillContext {
478
491
}
479
492
// The active view should change.
480
493
XCTAssertNotEqual (textInputPlugin.textInputView , textInputPlugin.autofillContext [@" field1" ]);
494
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
481
495
482
496
// Switch to a similar password field, the previous field should be reused.
483
497
oldContext = textInputPlugin.autofillContext ;
@@ -486,13 +500,16 @@ - (void)testAutofillContext {
486
500
// Reuse the input view instance from the last time.
487
501
XCTAssertEqual (self.viewsVisibleToAutofill .count , 1 );
488
502
XCTAssertEqual (textInputPlugin.autofillContext .count , 3 );
503
+
504
+ [textInputPlugin collectGarbageInputViews ];
489
505
XCTAssertEqual (self.installedInputViews .count , 4 );
490
506
491
507
// Old autofill input fields are still installed and reused.
492
508
for (NSString * key in oldContext.allKeys ) {
493
509
XCTAssertEqual (oldContext[key], textInputPlugin.autofillContext [key]);
494
510
}
495
511
XCTAssertNotEqual (textInputPlugin.textInputView , textInputPlugin.autofillContext [@" field1" ]);
512
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
496
513
}
497
514
498
515
- (void )testCommitAutofillContext {
@@ -526,21 +543,27 @@ - (void)testCommitAutofillContext {
526
543
[self setClientId: 123 configuration: config];
527
544
XCTAssertEqual (self.viewsVisibleToAutofill .count , 2 );
528
545
XCTAssertEqual (textInputPlugin.autofillContext .count , 2 );
546
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
529
547
530
548
[self commitAutofillContextAndVerify ];
531
549
XCTAssertNotEqual (textInputPlugin.textInputView , textInputPlugin.reusableInputView );
550
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
532
551
533
552
// Install the password field again.
534
553
[self setClientId: 123 configuration: config];
535
554
// Switch to a regular autofill group.
536
555
[self setClientId: 124 configuration: field3];
537
556
XCTAssertEqual (self.viewsVisibleToAutofill .count , 1 );
557
+
558
+ [textInputPlugin collectGarbageInputViews ];
538
559
XCTAssertEqual (self.installedInputViews .count , 3 );
539
560
XCTAssertEqual (textInputPlugin.autofillContext .count , 2 );
540
561
XCTAssertNotEqual (textInputPlugin.textInputView , nil );
562
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
541
563
542
564
[self commitAutofillContextAndVerify ];
543
565
XCTAssertNotEqual (textInputPlugin.textInputView , textInputPlugin.reusableInputView );
566
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
544
567
545
568
// Now switch to an input field that does not autofill.
546
569
[self setClientId: 125 configuration: self .mutableTemplateCopy];
@@ -549,11 +572,15 @@ - (void)testCommitAutofillContext {
549
572
XCTAssertEqual (textInputPlugin.textInputView , textInputPlugin.reusableInputView );
550
573
// The active view should still be installed so it doesn't get
551
574
// deallocated.
575
+
576
+ [textInputPlugin collectGarbageInputViews ];
552
577
XCTAssertEqual (self.installedInputViews .count , 1 );
553
578
XCTAssertEqual (textInputPlugin.autofillContext .count , 0 );
579
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
554
580
555
581
[self commitAutofillContextAndVerify ];
556
582
XCTAssertEqual (textInputPlugin.textInputView , textInputPlugin.reusableInputView );
583
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
557
584
}
558
585
559
586
- (void )testAutofillInputViews {
@@ -577,6 +604,7 @@ - (void)testAutofillInputViews {
577
604
[config setValue: @[ field1, field2 ] forKey: @" fields" ];
578
605
579
606
[self setClientId: 123 configuration: config];
607
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
580
608
581
609
// Find all the FlutterTextInputViews we created.
582
610
NSArray <FlutterTextInputView*>* inputFields = self.installedInputViews ;
@@ -589,6 +617,7 @@ - (void)testAutofillInputViews {
589
617
FlutterTextInputView* inactiveView = inputFields[1 ];
590
618
[inactiveView replaceRange: [FlutterTextRange rangeWithNSRange: NSMakeRange (0 , 0 )]
591
619
withText: @" Autofilled!" ];
620
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
592
621
593
622
// Verify behavior.
594
623
OCMVerify ([engine updateEditingClient: 0 withState: [OCMArg isNotNil ] withTag: @" field2" ]);
@@ -610,4 +639,61 @@ - (void)testPasswordAutofillHack {
610
639
XCTAssertNotEqual ([inputView performSelector: @selector (font )], nil );
611
640
}
612
641
642
+ - (void )testClearAutofillContextClearsSelection {
643
+ NSMutableDictionary * regularField = self.mutableTemplateCopy ;
644
+ NSDictionary * editingValue = @{
645
+ @" text" : @" REGULAR_TEXT_FIELD" ,
646
+ @" composingBase" : @0 ,
647
+ @" composingExtent" : @3 ,
648
+ @" selectionBase" : @1 ,
649
+ @" selectionExtent" : @4
650
+ };
651
+ [regularField setValue: @{
652
+ @" uniqueIdentifier" : @" field2" ,
653
+ @" hints" : @[ @" hint2" ],
654
+ @" editingValue" : editingValue,
655
+ }
656
+ forKey: @" autofill" ];
657
+ [regularField addEntriesFromDictionary: editingValue];
658
+ [self setClientId: 123 configuration: regularField];
659
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
660
+ XCTAssertEqual (self.installedInputViews .count , 1 );
661
+
662
+ FlutterTextInputView* oldInputView = self.installedInputViews [0 ];
663
+ XCTAssert ([oldInputView.text isEqualToString: @" REGULAR_TEXT_FIELD" ]);
664
+ FlutterTextRange* selectionRange = (FlutterTextRange*)oldInputView.selectedTextRange ;
665
+ XCTAssert (NSEqualRanges (selectionRange.range , NSMakeRange (1 , 3 )));
666
+
667
+ // Replace the original password field with new one. This should remove
668
+ // the old password field, but not immediately.
669
+ [self setClientId: 124 configuration: self .mutablePasswordTemplateCopy];
670
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
671
+
672
+ XCTAssertEqual (self.installedInputViews .count , 2 );
673
+
674
+ [textInputPlugin collectGarbageInputViews ];
675
+ XCTAssertEqual (self.installedInputViews .count , 1 );
676
+
677
+ // Verify the old input view is properly cleaned up.
678
+ XCTAssert ([oldInputView.text isEqualToString: @" " ]);
679
+ selectionRange = (FlutterTextRange*)oldInputView.selectedTextRange ;
680
+ XCTAssert (NSEqualRanges (selectionRange.range , NSMakeRange (0 , 0 )));
681
+ }
682
+
683
+ - (void )testGarbageInputViewsAreNotRemovedImmediately {
684
+ // Add a password field that should autofill.
685
+ [self setClientId: 123 configuration: self .mutablePasswordTemplateCopy];
686
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
687
+
688
+ XCTAssertEqual (self.installedInputViews .count , 1 );
689
+ // Add an input field that doesn't autofill. This should remove the password
690
+ // field, but not immediately.
691
+ [self setClientId: 124 configuration: self .mutableTemplateCopy];
692
+ [self ensureOnlyActiveViewCanBecomeFirstResponder ];
693
+
694
+ XCTAssertEqual (self.installedInputViews .count , 2 );
695
+
696
+ [self commitAutofillContextAndVerify ];
697
+ }
698
+
613
699
@end
0 commit comments