[iOS][MF] Add tests with an undocked keyboard

Bug: 924100
Change-Id: I76b42f78250d74d6d44e4878c3baf859870445fa
Reviewed-on: https://chromium-review.googlesource.com/c/1446353
Reviewed-by: David Jean <djean@chromium.org>
Reviewed-by: Rohit Rao <rohitrao@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#627912}(cherry picked from commit 6ed5405c7cc5532606304c56363e14eafe8c3703)
Reviewed-on: https://chromium-review.googlesource.com/c/1452857
Reviewed-by: Javier Ernesto Flores Robles <javierrobles@chromium.org>
Cr-Commit-Position: refs/branch-heads/3683@{#183}
Cr-Branched-From: e51029943e0a38dd794b73caaf6373d5496ae783-refs/heads/master@{#625896}
diff --git a/ios/chrome/browser/ui/autofill/manual_fill/fallback_coordinator_egtest.mm b/ios/chrome/browser/ui/autofill/manual_fill/fallback_coordinator_egtest.mm
index dc2e797..8323ddb 100644
--- a/ios/chrome/browser/ui/autofill/manual_fill/fallback_coordinator_egtest.mm
+++ b/ios/chrome/browser/ui/autofill/manual_fill/fallback_coordinator_egtest.mm
@@ -3,7 +3,9 @@
 // found in the LICENSE file.
 
 #import <EarlGrey/EarlGrey.h>
+#import <EarlGrey/GREYAppleInternals.h>
 #import <EarlGrey/GREYKeyboard.h>
+#include <atomic>
 
 #include "base/ios/ios_util.h"
 #include "base/strings/sys_string_conversions.h"
@@ -38,9 +40,14 @@
 namespace {
 
 constexpr char kFormElementName[] = "name";
+constexpr char kFormElementCity[] = "city";
 
 constexpr char kFormHTMLFile[] = "/profile_form.html";
 
+// EarlGrey fails to detect undocked keyboards on screen, so this help check
+// for them.
+static std::atomic_bool gCHRIsKeyboardShown(false);
+
 // Returns a matcher for the profiles icon in the keyboard accessory bar.
 id<GREYMatcher> ProfilesIconMatcher() {
   return grey_accessibilityID(
@@ -65,6 +72,13 @@
   return grey_allOf(classMatcher, parentMatcher, nil);
 }
 
+// Returns a matcher for a button in the ProfileTableView. Currently it returns
+// the company one.
+id<GREYMatcher> ProfileTableViewButtonMatcher() {
+  // The company name for autofill::test::GetFullProfile() is "Underworld".
+  return grey_buttonTitle(@"Underworld");
+}
+
 // Saves an example profile in the store.
 void AddAutofillProfile(autofill::PersonalDataManager* personalDataManager) {
   autofill::AutofillProfile profile = autofill::test::GetFullProfile();
@@ -95,6 +109,156 @@
   return [condition waitWithTimeout:timeout];
 }
 
+// If the keyboard is not present this will add a text field to the hierarchy,
+// make it first responder and return it. If it is already present, this does
+// nothing and returns nil.
+UITextField* ShowKeyboard() {
+  UITextField* textField = nil;
+  if (!gCHRIsKeyboardShown) {
+    CGRect rect = CGRectMake(0, 0, 300, 100);
+    textField = [[UITextField alloc] initWithFrame:rect];
+    textField.backgroundColor = [UIColor blueColor];
+    [[[UIApplication sharedApplication] keyWindow] addSubview:textField];
+    [textField becomeFirstResponder];
+  }
+  auto verify_block = ^BOOL {
+    return gCHRIsKeyboardShown;
+  };
+  NSTimeInterval timeout = base::test::ios::kWaitForUIElementTimeout;
+  NSString* condition_name =
+      [NSString stringWithFormat:@"Wait for keyboard to appear"];
+  GREYCondition* condition = [GREYCondition conditionWithName:condition_name
+                                                        block:verify_block];
+  [condition waitWithTimeout:timeout];
+  return textField;
+}
+
+// Returns the dismiss key if present in the passed keyboard layout. Returns nil
+// if not found.
+UIAccessibilityElement* KeyboardDismissKeyInLayout(UIView* layout) {
+  UIAccessibilityElement* key = nil;
+  if ([layout accessibilityElementCount] != NSNotFound) {
+    for (NSInteger i = [layout accessibilityElementCount]; i >= 0; --i) {
+      id element = [layout accessibilityElementAtIndex:i];
+      if ([[[element key] valueForKey:@"name"]
+              isEqualToString:@"Dismiss-Key"]) {
+        key = element;
+        break;
+      }
+    }
+  }
+  return key;
+}
+
+// Finds the first view containing the keyboard which origin is not zero.
+UIView* KeyboardContainerForLayout(UIView* layout) {
+  CGRect frame = CGRectZero;
+  UIView* keyboardContainer = layout;
+  while (CGPointEqualToPoint(frame.origin, CGPointZero) && keyboardContainer) {
+    keyboardContainer = [keyboardContainer superview];
+    if (keyboardContainer) {
+      frame = keyboardContainer.frame;
+    }
+  }
+  return keyboardContainer;
+}
+
+// Returns YES if the keyboard is docked at the bottom. NO otherwise.
+BOOL IsKeyboardDockedForLayout(UIView* layout) {
+  UIView* keyboardContainer = KeyboardContainerForLayout(layout);
+  CGRect screenBounds = [[UIScreen mainScreen] bounds];
+  CGFloat maxY = CGRectGetMaxY(keyboardContainer.frame);
+  return [@(maxY) isEqualToNumber:@(screenBounds.size.height)];
+}
+
+// Undocks the keyboard by swiping it up. Does nothing if already undocked.
+void UndockKeyboard() {
+  if (!IsIPadIdiom()) {
+    return;
+  }
+
+  UITextField* textField = ShowKeyboard();
+
+  // Assert the "Dismiss-Key" is present.
+  UIView* layout = [[UIKeyboardImpl sharedInstance] _layout];
+  GREYAssert([[layout valueForKey:@"keyplaneContainsDismissKey"] boolValue],
+             @"No dismiss key is pressent");
+
+  // Return if already undocked.
+  if (!IsKeyboardDockedForLayout(layout)) {
+    // If we created a dummy textfield for this, remove it.
+    [textField removeFromSuperview];
+    return;
+  }
+
+  // Swipe it up.
+  if (!layout.accessibilityIdentifier.length) {
+    layout.accessibilityIdentifier = @"CRKBLayout";
+  }
+
+  id<GREYMatcher> matcher =
+      grey_accessibilityID(layout.accessibilityIdentifier);
+
+  UIAccessibilityElement* key = KeyboardDismissKeyInLayout(layout);
+  CGRect keyFrame = [key accessibilityFrame];
+  CGRect keyboardContainerFrame = KeyboardContainerForLayout(layout).frame;
+  CGPoint pointToKey = {keyFrame.origin.x - keyboardContainerFrame.origin.x,
+                        keyFrame.origin.y - keyboardContainerFrame.origin.y};
+  CGRectIntersection(keyFrame, keyboardContainerFrame);
+  CGPoint startPoint = CGPointMake((pointToKey.x + keyFrame.size.width / 2.0) /
+                                       keyboardContainerFrame.size.width,
+                                   (pointToKey.y + keyFrame.size.height / 2.0) /
+                                       keyboardContainerFrame.size.height);
+
+  id action = grey_swipeFastInDirectionWithStartPoint(
+      kGREYDirectionUp, startPoint.x, startPoint.y);
+  [[EarlGrey selectElementWithMatcher:matcher] performAction:action];
+}
+
+// Docks the keyboard by swiping it down. Does nothing if already docked.
+void DockKeyboard() {
+  if (!IsIPadIdiom()) {
+    return;
+  }
+
+  UITextField* textField = ShowKeyboard();
+
+  // Assert the "Dismiss-Key" is present.
+  UIView* layout = [[UIKeyboardImpl sharedInstance] _layout];
+  GREYAssert([[layout valueForKey:@"keyplaneContainsDismissKey"] boolValue],
+             @"No dismiss key is pressent");
+
+  // Return if already docked.
+  if (IsKeyboardDockedForLayout(layout)) {
+    // If we created a dummy textfield for this, remove it.
+    [textField removeFromSuperview];
+    return;
+  }
+
+  // Swipe it down.
+  id<GREYMatcher> classMatcher = grey_kindOfClass([UIWindow class]);
+  UIAccessibilityElement* key = KeyboardDismissKeyInLayout(layout);
+  id<GREYMatcher> parentMatcher =
+      grey_descendant(grey_accessibilityLabel(key.accessibilityLabel));
+  id matcher = grey_allOf(classMatcher, parentMatcher, nil);
+
+  CGRect keyFrame = [key accessibilityFrame];
+  GREYAssertFalse(CGRectEqualToRect(keyFrame, CGRectZero),
+                  @"The dismiss key accessibility frame musn't be zero");
+  CGPoint startPoint =
+      CGPointMake((keyFrame.origin.x + keyFrame.size.width / 2.0) /
+                      [UIScreen mainScreen].bounds.size.width,
+                  (keyFrame.origin.y + keyFrame.size.height / 2.0) /
+                      [UIScreen mainScreen].bounds.size.height);
+  id<GREYAction> action = grey_swipeFastInDirectionWithStartPoint(
+      kGREYDirectionDown, startPoint.x, startPoint.y);
+
+  [[EarlGrey selectElementWithMatcher:matcher] performAction:action];
+
+  // If we created a dummy textfield for this, remove it.
+  [textField removeFromSuperview];
+}
+
 }  // namespace
 
 // Integration Tests for fallback coordinator.
@@ -107,6 +271,41 @@
 
 @implementation FallbackCoordinatorTestCase
 
++ (void)load {
+  @autoreleasepool {
+    // EarlGrey fails to detect undocked keyboards on screen, so this help check
+    // for them.
+    auto block = ^(NSNotification* note) {
+      CGRect keyboardFrame =
+          [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
+      UIWindow* window = [UIApplication sharedApplication].keyWindow;
+      keyboardFrame = [window convertRect:keyboardFrame fromWindow:nil];
+      CGRect windowFrame = window.frame;
+      CGRect frameIntersection = CGRectIntersection(windowFrame, keyboardFrame);
+      gCHRIsKeyboardShown =
+          frameIntersection.size.width > 1 && frameIntersection.size.height > 1;
+    };
+
+    [[NSNotificationCenter defaultCenter]
+        addObserverForName:UIKeyboardDidChangeFrameNotification
+                    object:nil
+                     queue:nil
+                usingBlock:block];
+
+    [[NSNotificationCenter defaultCenter]
+        addObserverForName:UIKeyboardDidShowNotification
+                    object:nil
+                     queue:nil
+                usingBlock:block];
+
+    [[NSNotificationCenter defaultCenter]
+        addObserverForName:UIKeyboardDidHideNotification
+                    object:nil
+                     queue:nil
+                usingBlock:block];
+  }
+}
+
 - (void)setUp {
   [super setUp];
   GREYAssert(autofill::features::IsAutofillManualFallbackEnabled(),
@@ -129,6 +328,18 @@
   for (const auto* profile : _personalDataManager->GetProfiles()) {
     _personalDataManager->RemoveByGUID(profile->guid());
   }
+  // Leaving a picker on iPads causes problems with the docking logic. This
+  // will dismiss any.
+  if (IsIPadIdiom()) {
+    // Tap in the web view so the popover dismisses.
+    [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+        performAction:grey_tapAtPoint(CGPointMake(0, 0))];
+
+    // Verify the table view is not visible.
+    [[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITableView class])]
+        assertWithMatcher:grey_notVisible()];
+  }
+  DockKeyboard();
   [super tearDown];
 }
 
@@ -136,7 +347,7 @@
 // continue working.
 - (void)testIPadTappingOutsidePopOverResumesSuggestionsCorrectly {
   if (!IsIPadIdiom()) {
-    return;
+    EARL_GREY_TEST_SKIPPED(@"Test not applicable for iPhone.");
   }
 
   GREYAssertEqual(_personalDataManager->GetProfiles().size(), 0,
@@ -184,4 +395,110 @@
   XCTAssertTrue(WaitForJavaScriptCondition(javaScriptCondition));
 }
 
+// Tests that the manual fallback view concedes preference to the system picker
+// for selection elements.
+- (void)testPickerDismissesManualFallback {
+  // Add the profile to be used.
+  AddAutofillProfile(_personalDataManager);
+
+  // Bring up the keyboard.
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+      performAction:chrome_test_util::TapWebElement(kFormElementCity)];
+
+  // Tap on the profiles icon.
+  [[EarlGrey selectElementWithMatcher:ProfilesIconMatcher()]
+      performAction:grey_tap()];
+
+  // Verify the profiles controller table view is visible.
+  [[EarlGrey selectElementWithMatcher:ProfilesTableViewMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Tap any option.
+  [[EarlGrey selectElementWithMatcher:ProfileTableViewButtonMatcher()]
+      performAction:grey_tap()];
+
+  // Verify the profiles controller table view is not visible.
+  [[EarlGrey selectElementWithMatcher:ProfilesTableViewMatcher()]
+      assertWithMatcher:grey_notVisible()];
+}
+
+// Tests that the input accessory view continues working after a picker is
+// present.
+- (void)testInputAccessoryBarIsPresentAfterPickers {
+  // Add the profile to be used.
+  AddAutofillProfile(_personalDataManager);
+
+  // Bring up the keyboard by tapping the city, which is the element before the
+  // picker.
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+      performAction:chrome_test_util::TapWebElement(kFormElementCity)];
+
+  // Tap on the profiles icon.
+  [[EarlGrey selectElementWithMatcher:ProfilesIconMatcher()]
+      performAction:grey_tap()];
+
+  // Verify the profiles controller table view is visible.
+  [[EarlGrey selectElementWithMatcher:ProfilesTableViewMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  // Tap any option.
+  [[EarlGrey selectElementWithMatcher:ProfileTableViewButtonMatcher()]
+      performAction:grey_tap()];
+
+  // Verify the profiles controller table view is not visible.
+  [[EarlGrey selectElementWithMatcher:ProfilesTableViewMatcher()]
+      assertWithMatcher:grey_notVisible()];
+
+  // On iPad the picker is a table view in a popover, we need to dismiss that
+  // first.
+  if (IsIPadIdiom()) {
+    // Tap in the web view so the popover dismisses.
+    [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+        performAction:grey_tapAtPoint(CGPointMake(0, 0))];
+
+    // Verify the table view is not visible.
+    [[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITableView class])]
+        assertWithMatcher:grey_notVisible()];
+  }
+
+  // Bring up the regular keyboard again.
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+      performAction:chrome_test_util::TapWebElement(kFormElementName)];
+
+  // Wait for the accessory icon to appear.
+  [GREYKeyboard waitForKeyboardToAppear];
+
+  // Verify the profiles icon is visible, and therefore also the input accessory
+  // bar.
+  [[EarlGrey selectElementWithMatcher:ProfilesIconMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+}
+
+// Test the input accessory bar is present when undocking the keyboard.
+- (void)testInputAccessoryBarIsPresentAfterUndockingKeyboard {
+  if (!IsIPadIdiom()) {
+    EARL_GREY_TEST_SKIPPED(@"Test not applicable for iPhone.");
+  }
+  // Add the profile to use for verification.
+  AddAutofillProfile(_personalDataManager);
+
+  // Bring up the keyboard by tapping the city, which is the element before the
+  // picker.
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
+      performAction:chrome_test_util::TapWebElement(kFormElementCity)];
+
+  UndockKeyboard();
+
+  // Verify the profiles icon.
+  [[EarlGrey selectElementWithMatcher:ProfilesIconMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+
+  DockKeyboard();
+
+  // Verify the profiles icon is visible, and therefore also the input accessory
+  // bar.
+  [[EarlGrey selectElementWithMatcher:ProfilesIconMatcher()]
+      assertWithMatcher:grey_sufficientlyVisible()];
+}
+
 @end
diff --git a/ios/third_party/earl_grey/BUILD.gn b/ios/third_party/earl_grey/BUILD.gn
index 893f13e..5b1c3ec 100644
--- a/ios/third_party/earl_grey/BUILD.gn
+++ b/ios/third_party/earl_grey/BUILD.gn
@@ -253,6 +253,7 @@
     "src/EarlGrey/Assertion/GREYAssertionBlock.h",
     "src/EarlGrey/Assertion/GREYAssertionDefines.h",
     "src/EarlGrey/Assertion/GREYAssertions.h",
+    "src/EarlGrey/Common/GREYAppleInternals.h",
     "src/EarlGrey/Common/GREYConfiguration.h",
     "src/EarlGrey/Common/GREYConstants.h",
     "src/EarlGrey/Common/GREYDefines.h",