Commit 47c75629 authored by Javier Ernesto Flores Robles's avatar Javier Ernesto Flores Robles Committed by Commit Bot

[iOS][MF] Pause and restore keyboard custom view

This CL adds the capability to pause the keyboard views when the
webstate is hidden. This happens i.e. when the passwords coordinator
presents a full screen selection. This way, when the webstate is shown
again, it can restore it's views if needed.

Bug: 845472
Cq-Include-Trybots: luci.chromium.try:ios-simulator-cronet;luci.chromium.try:ios-simulator-full-configs
Change-Id: I5eddf31ada277a1a9c0c0e8cc0ae7be3d8d01ad2
Reviewed-on: https://chromium-review.googlesource.com/c/1236436
Commit-Queue: Javier Ernesto Flores Robles <javierrobles@chromium.org>
Reviewed-by: default avatarEric Noyau <noyau@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#596648}
parent 29ce3d0f
...@@ -14,9 +14,17 @@ ...@@ -14,9 +14,17 @@
// Removes the animations on the custom keyboard view. // Removes the animations on the custom keyboard view.
- (void)removeAnimationsOnKeyboardView; - (void)removeAnimationsOnKeyboardView;
// Restores the keyboard and its default input accessory view, removing (if // Removes the presented keyboard view and the input accessory view. Also clears
// necessary) any previously-added custom view. // the references to them, so nothing shows until a new custom view is passed.
- (void)restoreKeyboardView; - (void)restoreOriginalKeyboardView;
// Removes the presented keyboard view and the input accessory view until
// |continueCustomKeyboardView| is called.
- (void)pauseCustomKeyboardView;
// Adds the previously presented views to the keyboard. If they have not been
// reset.
- (void)continueCustomKeyboardView;
// Hides the default input accessory view and replaces it with one that shows // Hides the default input accessory view and replaces it with one that shows
// |customView| and form navigation controls. // |customView| and form navigation controls.
......
...@@ -29,6 +29,12 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -29,6 +29,12 @@ CGFloat const kInputAccessoryHeight = 44.0f;
// The keyboard replacement view, if any. // The keyboard replacement view, if any.
@property(nonatomic, weak) UIView* keyboardReplacementView; @property(nonatomic, weak) UIView* keyboardReplacementView;
// The custom view that should be shown in the input accessory view.
@property(nonatomic, strong) FormInputAccessoryView* customAccessoryView;
// If this view controller is paused it shouldn't add its views to the keyboard.
@property(nonatomic, getter=isPaused) BOOL paused;
// Called when the keyboard will or did change frame. // Called when the keyboard will or did change frame.
- (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification; - (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification;
...@@ -38,16 +44,10 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -38,16 +44,10 @@ CGFloat const kInputAccessoryHeight = 44.0f;
// Last registered keyboard rectangle. // Last registered keyboard rectangle.
CGRect _keyboardFrame; CGRect _keyboardFrame;
// The custom view that should be shown in the input accessory view.
FormInputAccessoryView* _customAccessoryView;
// Whether suggestions have previously been shown. // Whether suggestions have previously been shown.
BOOL _suggestionsHaveBeenShown; BOOL _suggestionsHaveBeenShown;
} }
@synthesize grayBackgroundView = _grayBackgroundView;
@synthesize keyboardReplacementView = _keyboardReplacementView;
#pragma mark - Life Cycle #pragma mark - Life Cycle
- (instancetype)init { - (instancetype)init {
...@@ -88,6 +88,9 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -88,6 +88,9 @@ CGFloat const kInputAccessoryHeight = 44.0f;
#pragma mark - Public #pragma mark - Public
- (void)presentView:(UIView*)view { - (void)presentView:(UIView*)view {
if (self.paused) {
return;
}
DCHECK(view); DCHECK(view);
DCHECK(!view.superview); DCHECK(!view.superview);
UIView* keyboardView = [self getKeyboardView]; UIView* keyboardView = [self getKeyboardView];
...@@ -110,14 +113,14 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -110,14 +113,14 @@ CGFloat const kInputAccessoryHeight = 44.0f;
if (IsIPadIdiom()) { if (IsIPadIdiom()) {
// On iPad, there's no inputAccessoryView available, so we attach the custom // On iPad, there's no inputAccessoryView available, so we attach the custom
// view directly to the keyboard view instead. // view directly to the keyboard view instead.
[_customAccessoryView removeFromSuperview]; [self.customAccessoryView removeFromSuperview];
[self.grayBackgroundView removeFromSuperview]; [self.grayBackgroundView removeFromSuperview];
// If the keyboard isn't visible don't show the custom view. // If the keyboard isn't visible don't show the custom view.
if (CGRectIntersection([UIScreen mainScreen].bounds, _keyboardFrame) if (CGRectIntersection([UIScreen mainScreen].bounds, _keyboardFrame)
.size.height == 0 || .size.height == 0 ||
CGRectEqualToRect(_keyboardFrame, CGRectZero)) { CGRectEqualToRect(_keyboardFrame, CGRectZero)) {
_customAccessoryView = nil; self.customAccessoryView = nil;
return; return;
} }
...@@ -128,36 +131,41 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -128,36 +131,41 @@ CGFloat const kInputAccessoryHeight = 44.0f;
if (formSuggestionView) { if (formSuggestionView) {
int numSuggestions = [[formSuggestionView suggestions] count]; int numSuggestions = [[formSuggestionView suggestions] count];
if (!_suggestionsHaveBeenShown && numSuggestions == 0) { if (!_suggestionsHaveBeenShown && numSuggestions == 0) {
_customAccessoryView = nil; self.customAccessoryView = nil;
return; return;
} }
} }
_suggestionsHaveBeenShown = YES; _suggestionsHaveBeenShown = YES;
_customAccessoryView = [[FormInputAccessoryView alloc] init]; self.customAccessoryView = [[FormInputAccessoryView alloc] init];
[_customAccessoryView setUpWithCustomView:view]; [self.customAccessoryView setUpWithCustomView:view];
[self addCustomAccessoryViewIfNeeded]; [self addCustomAccessoryViewIfNeeded];
} else { } else {
// On iPhone, the custom view replaces the default UI of the // On iPhone, the custom view replaces the default UI of the
// inputAccessoryView. // inputAccessoryView.
[self restoreInputAccessoryView]; [self restoreOriginalInputAccessoryView];
_customAccessoryView = [[FormInputAccessoryView alloc] init]; self.customAccessoryView = [[FormInputAccessoryView alloc] init];
[_customAccessoryView setUpWithNavigationDelegate:navigationDelegate [self.customAccessoryView setUpWithNavigationDelegate:navigationDelegate
customView:view]; customView:view];
[self addCustomAccessoryViewIfNeeded]; [self addCustomAccessoryViewIfNeeded];
} }
} }
- (void)restoreInputAccessoryView { - (void)restoreOriginalKeyboardView {
[_customAccessoryView removeFromSuperview]; [self restoreOriginalInputAccessoryView];
[self.grayBackgroundView removeFromSuperview]; [self.keyboardReplacementView removeFromSuperview];
_customAccessoryView = nil; self.keyboardReplacementView = nil;
self.paused = NO;
} }
- (void)restoreKeyboardView { - (void)pauseCustomKeyboardView {
[self restoreInputAccessoryView]; [self removeCustomInputAccessoryView];
[self.keyboardReplacementView removeFromSuperview]; [self.keyboardReplacementView removeFromSuperview];
self.keyboardReplacementView = nil; self.paused = YES;
}
- (void)continueCustomKeyboardView {
self.paused = NO;
} }
- (void)removeAnimationsOnKeyboardView { - (void)removeAnimationsOnKeyboardView {
...@@ -170,6 +178,18 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -170,6 +178,18 @@ CGFloat const kInputAccessoryHeight = 44.0f;
#pragma mark - Private #pragma mark - Private
// Removes the custom views related to the input accessory view.
- (void)removeCustomInputAccessoryView {
[self.customAccessoryView removeFromSuperview];
[self.grayBackgroundView removeFromSuperview];
}
// Removes the custom input accessory views and clears the references.
- (void)restoreOriginalInputAccessoryView {
[self removeCustomInputAccessoryView];
self.customAccessoryView = nil;
}
// This searches in a keyboard view hierarchy for the best candidate to // This searches in a keyboard view hierarchy for the best candidate to
// constrain a view to the keyboard. // constrain a view to the keyboard.
- (UIView*)recursiveGetKeyboardConstraintView:(UIView*)view { - (UIView*)recursiveGetKeyboardConstraintView:(UIView*)view {
...@@ -230,9 +250,7 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -230,9 +250,7 @@ CGFloat const kInputAccessoryHeight = 44.0f;
// will appear. // will appear.
if (!IsIPadIdiom()) { if (!IsIPadIdiom()) {
[self addCustomAccessoryViewIfNeeded]; [self addCustomAccessoryViewIfNeeded];
} [self addCustomKeyboardViewIfNeeded];
if (self.keyboardReplacementView) {
[self presentView:self.keyboardReplacementView];
} }
} }
...@@ -251,53 +269,65 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -251,53 +269,65 @@ CGFloat const kInputAccessoryHeight = 44.0f;
} }
// On ipad we hide the views so they don't stick around at the bottom. Only // On ipad we hide the views so they don't stick around at the bottom. Only
// needed on iPad because we add the view directly to the keyboard view. // needed on iPad because we add the view directly to the keyboard view.
if (IsIPadIdiom() && _customAccessoryView) { if (IsIPadIdiom() && self.customAccessoryView) {
if (@available(iOS 11, *)) { if (@available(iOS 11, *)) {
} else { } else {
// [iPad iOS 10] There is a bug when constraining something to the // [iPad iOS 10] There is a bug when constraining something to the
// keyboard view. So this updates the frame instead. // keyboard view. So this updates the frame instead.
CGFloat height = autofill::kInputAccessoryHeight; CGFloat height = autofill::kInputAccessoryHeight;
_customAccessoryView.frame = self.customAccessoryView.frame =
CGRectMake(keyboardView.frame.origin.x, -height, CGRectMake(keyboardView.frame.origin.x, -height,
keyboardView.frame.size.width, height); keyboardView.frame.size.width, height);
} }
if (CGRectEqualToRect(_keyboardFrame, CGRectZero)) { if (CGRectEqualToRect(_keyboardFrame, CGRectZero)) {
_customAccessoryView.hidden = true; self.customAccessoryView.hidden = true;
self.grayBackgroundView.hidden = true; self.grayBackgroundView.hidden = true;
} else { } else {
_customAccessoryView.hidden = false; self.customAccessoryView.hidden = false;
self.grayBackgroundView.hidden = false; self.grayBackgroundView.hidden = false;
} }
} }
} }
- (void)addCustomKeyboardViewIfNeeded {
if (self.isPaused) {
return;
}
if (self.keyboardReplacementView && !self.keyboardReplacementView.superview) {
[self presentView:self.keyboardReplacementView];
}
}
// Adds the customAccessoryView and the backgroundView (on iPads), if those are // Adds the customAccessoryView and the backgroundView (on iPads), if those are
// not already in the hierarchy. // not already in the hierarchy.
- (void)addCustomAccessoryViewIfNeeded { - (void)addCustomAccessoryViewIfNeeded {
if (_customAccessoryView && !_customAccessoryView.superview) { if (self.isPaused) {
return;
}
if (self.customAccessoryView && !self.customAccessoryView.superview) {
if (IsIPadIdiom()) { if (IsIPadIdiom()) {
UIView* keyboardView = [self getKeyboardView]; UIView* keyboardView = [self getKeyboardView];
// [iPad iOS 10] There is a bug when constraining something to the // [iPad iOS 10] There is a bug when constraining something to the
// keyboard view. So this sets the frame instead. // keyboard view. So this sets the frame instead.
if (@available(iOS 11, *)) { if (@available(iOS 11, *)) {
_customAccessoryView.translatesAutoresizingMaskIntoConstraints = NO; self.customAccessoryView.translatesAutoresizingMaskIntoConstraints = NO;
[keyboardView addSubview:_customAccessoryView]; [keyboardView addSubview:self.customAccessoryView];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[_customAccessoryView.leadingAnchor [self.customAccessoryView.leadingAnchor
constraintEqualToAnchor:keyboardView.leadingAnchor], constraintEqualToAnchor:keyboardView.leadingAnchor],
[_customAccessoryView.trailingAnchor [self.customAccessoryView.trailingAnchor
constraintEqualToAnchor:keyboardView.trailingAnchor], constraintEqualToAnchor:keyboardView.trailingAnchor],
[_customAccessoryView.bottomAnchor [self.customAccessoryView.bottomAnchor
constraintEqualToAnchor:keyboardView.topAnchor], constraintEqualToAnchor:keyboardView.topAnchor],
[_customAccessoryView.heightAnchor [self.customAccessoryView.heightAnchor
constraintEqualToConstant:autofill::kInputAccessoryHeight] constraintEqualToConstant:autofill::kInputAccessoryHeight]
]]; ]];
} else { } else {
CGFloat height = autofill::kInputAccessoryHeight; CGFloat height = autofill::kInputAccessoryHeight;
_customAccessoryView.frame = self.customAccessoryView.frame =
CGRectMake(keyboardView.frame.origin.x, -height, CGRectMake(keyboardView.frame.origin.x, -height,
keyboardView.frame.size.width, height); keyboardView.frame.size.width, height);
[keyboardView addSubview:_customAccessoryView]; [keyboardView addSubview:self.customAccessoryView];
} }
if (!self.grayBackgroundView.superview) { if (!self.grayBackgroundView.superview) {
[keyboardView addSubview:self.grayBackgroundView]; [keyboardView addSubview:self.grayBackgroundView];
...@@ -308,8 +338,8 @@ CGFloat const kInputAccessoryHeight = 44.0f; ...@@ -308,8 +338,8 @@ CGFloat const kInputAccessoryHeight = 44.0f;
UIResponder* firstResponder = GetFirstResponder(); UIResponder* firstResponder = GetFirstResponder();
UIView* inputAccessoryView = firstResponder.inputAccessoryView; UIView* inputAccessoryView = firstResponder.inputAccessoryView;
if (inputAccessoryView) { if (inputAccessoryView) {
[inputAccessoryView addSubview:_customAccessoryView]; [inputAccessoryView addSubview:self.customAccessoryView];
AddSameConstraints(_customAccessoryView, inputAccessoryView); AddSameConstraints(self.customAccessoryView, inputAccessoryView);
} }
} }
} }
......
...@@ -208,7 +208,7 @@ class FormSuggestionControllerTest : public PlatformTest { ...@@ -208,7 +208,7 @@ class FormSuggestionControllerTest : public PlatformTest {
[view removeFromSuperview]; [view removeFromSuperview];
} }
}; };
[[[mock_consumer_ stub] andDo:mockRestore] restoreKeyboardView]; [[[mock_consumer_ stub] andDo:mockRestore] restoreOriginalKeyboardView];
accessory_mediator_ = accessory_mediator_ =
[[FormInputAccessoryMediator alloc] initWithConsumer:mock_consumer_ [[FormInputAccessoryMediator alloc] initWithConsumer:mock_consumer_
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
- (void)stop { - (void)stop {
[self stopChildren]; [self stopChildren];
[self.manualFillAccessoryViewController reset]; [self.manualFillAccessoryViewController reset];
[self.formInputAccessoryViewController restoreKeyboardView]; [self.formInputAccessoryViewController restoreOriginalKeyboardView];
} }
#pragma mark - Presenting Children #pragma mark - Presenting Children
......
...@@ -164,7 +164,9 @@ ...@@ -164,7 +164,9 @@
} }
- (void)keyboardDidHide { - (void)keyboardDidHide {
if (_webState && _webState->IsVisible()) {
[self reset]; [self reset];
}
} }
#pragma mark - FormActivityObserver #pragma mark - FormActivityObserver
...@@ -198,6 +200,11 @@ ...@@ -198,6 +200,11 @@
#pragma mark - CRWWebStateObserver #pragma mark - CRWWebStateObserver
- (void)webStateWasShown:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self.consumer continueCustomKeyboardView];
}
- (void)webStateWasHidden:(web::WebState*)webState { - (void)webStateWasHidden:(web::WebState*)webState {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
// On some iPhone with newers iOS (>11.3) when a view controller is presented, // On some iPhone with newers iOS (>11.3) when a view controller is presented,
...@@ -209,6 +216,8 @@ ...@@ -209,6 +216,8 @@
// element gets the focus. On iPad the keyboard stays dismissed. // element gets the focus. On iPad the keyboard stays dismissed.
if (IsIPadIdiom()) { if (IsIPadIdiom()) {
[self reset]; [self reset];
} else {
[self.consumer pauseCustomKeyboardView];
} }
} }
...@@ -229,6 +238,7 @@ ...@@ -229,6 +238,7 @@
oldWebState:(web::WebState*)oldWebState oldWebState:(web::WebState*)oldWebState
atIndex:(int)atIndex atIndex:(int)atIndex
reason:(int)reason { reason:(int)reason {
[self reset];
[self updateWithNewWebState:newWebState]; [self updateWithNewWebState:newWebState];
} }
...@@ -289,7 +299,7 @@ ...@@ -289,7 +299,7 @@
// Resets the current provider, the consumer view and the navigation handler. As // Resets the current provider, the consumer view and the navigation handler. As
// well as reenables suggestions. // well as reenables suggestions.
- (void)reset { - (void)reset {
[self.consumer restoreKeyboardView]; [self.consumer restoreOriginalKeyboardView];
[self.manualFillAccessoryViewController reset]; [self.manualFillAccessoryViewController reset];
[self.formInputAccessoryHandler reset]; [self.formInputAccessoryHandler reset];
...@@ -400,7 +410,7 @@ queryViewBlockForProvider:(id<FormInputAccessoryViewProvider>)provider ...@@ -400,7 +410,7 @@ queryViewBlockForProvider:(id<FormInputAccessoryViewProvider>)provider
// begins editing, reset ourselves so that we don't present our custom view over // begins editing, reset ourselves so that we don't present our custom view over
// the keyboard. // the keyboard.
- (void)handleTextInputDidBeginEditing:(NSNotification*)notification { - (void)handleTextInputDidBeginEditing:(NSNotification*)notification {
[self reset]; [self.consumer pauseCustomKeyboardView];
} }
#pragma mark - Tests #pragma mark - Tests
......
...@@ -119,6 +119,9 @@ source_set("eg_tests") { ...@@ -119,6 +119,9 @@ source_set("eg_tests") {
"//base", "//base",
"//base/test:test_support", "//base/test:test_support",
"//components/autofill/core/common", "//components/autofill/core/common",
"//components/keyed_service/core",
"//components/password_manager/core/browser",
"//ios/chrome/browser/passwords",
"//ios/chrome/browser/ui:ui_util", "//ios/chrome/browser/ui:ui_util",
"//ios/chrome/test/app:test_support", "//ios/chrome/test/app:test_support",
"//ios/chrome/test/earl_grey:test_support", "//ios/chrome/test/earl_grey:test_support",
......
...@@ -22,6 +22,7 @@ class WebStateList; ...@@ -22,6 +22,7 @@ class WebStateList;
namespace manual_fill { namespace manual_fill {
extern NSString* const ManagePasswordsAccessibilityIdentifier; extern NSString* const ManagePasswordsAccessibilityIdentifier;
extern NSString* const OtherPasswordsAccessibilityIdentifier;
} // namespace manual_fill } // namespace manual_fill
......
...@@ -33,6 +33,8 @@ namespace manual_fill { ...@@ -33,6 +33,8 @@ namespace manual_fill {
NSString* const ManagePasswordsAccessibilityIdentifier = NSString* const ManagePasswordsAccessibilityIdentifier =
@"kManualFillManagePasswordsAccessibilityIdentifier"; @"kManualFillManagePasswordsAccessibilityIdentifier";
NSString* const OtherPasswordsAccessibilityIdentifier =
@"kManualFillOtherPasswordsAccessibilityIdentifier";
} // namespace manual_fill } // namespace manual_fill
...@@ -182,6 +184,8 @@ NSString* const ManagePasswordsAccessibilityIdentifier = ...@@ -182,6 +184,8 @@ NSString* const ManagePasswordsAccessibilityIdentifier =
action:^{ action:^{
[weakSelf.navigationDelegate openAllPasswordsList]; [weakSelf.navigationDelegate openAllPasswordsList];
}]; }];
otherPasswordsItem.accessibilityIdentifier =
manual_fill::OtherPasswordsAccessibilityIdentifier;
NSString* managePasswordsTitle = NSString* managePasswordsTitle =
l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_MANAGE_PASSWORDS); l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_MANAGE_PASSWORDS);
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
namespace manual_fill { namespace manual_fill {
extern NSString* const PasswordSearchBarAccessibilityIdentifier;
extern NSString* const PasswordTableViewAccessibilityIdentifier; extern NSString* const PasswordTableViewAccessibilityIdentifier;
} // namespace manual_fill } // namespace manual_fill
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
namespace manual_fill { namespace manual_fill {
NSString* const PasswordSearchBarAccessibilityIdentifier =
@"kManualFillPasswordSearchBarAccessibilityIdentifier";
NSString* const PasswordTableViewAccessibilityIdentifier = NSString* const PasswordTableViewAccessibilityIdentifier =
@"kManualFillPasswordTableViewAccessibilityIdentifier"; @"kManualFillPasswordTableViewAccessibilityIdentifier";
...@@ -73,6 +75,8 @@ typedef NS_ENUM(NSInteger, SectionIdentifier) { ...@@ -73,6 +75,8 @@ typedef NS_ENUM(NSInteger, SectionIdentifier) {
} else { } else {
self.tableView.tableHeaderView = self.searchController.searchBar; self.tableView.tableHeaderView = self.searchController.searchBar;
} }
self.searchController.searchBar.accessibilityIdentifier =
manual_fill::PasswordSearchBarAccessibilityIdentifier;
NSString* titleString = NSString* titleString =
l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_USE_OTHER_PASSWORD); l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_USE_OTHER_PASSWORD);
self.title = titleString; self.title = titleString;
......
...@@ -3,9 +3,18 @@ ...@@ -3,9 +3,18 @@
// found in the LICENSE file. // found in the LICENSE file.
#import <EarlGrey/EarlGrey.h> #import <EarlGrey/EarlGrey.h>
#import <EarlGrey/GREYKeyboard.h>
#include "base/ios/ios_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h" #import "base/test/ios/wait_util.h"
#include "components/autofill/core/common/autofill_features.h" #include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/password_form.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h"
#import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_accessory_view_controller.h" #import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_accessory_view_controller.h"
#import "ios/chrome/browser/ui/autofill/manual_fill/password_mediator.h" #import "ios/chrome/browser/ui/autofill/manual_fill/password_mediator.h"
#import "ios/chrome/browser/ui/autofill/manual_fill/password_view_controller.h" #import "ios/chrome/browser/ui/autofill/manual_fill/password_view_controller.h"
...@@ -26,9 +35,13 @@ ...@@ -26,9 +35,13 @@
namespace { namespace {
const char kFormHTMLFile[] = "/username_password_field_form.html";
const char kFormElementUsername[] = "username"; const char kFormElementUsername[] = "username";
const char kExampleUsername[] = "concrete username";
const char kExamplePassword[] = "concrete password";
const char kFormHTMLFile[] = "/username_password_field_form.html";
// Returns a matcher for the password icon in the keyboard accessory bar. // Returns a matcher for the password icon in the keyboard accessory bar.
id<GREYMatcher> PasswordIconMatcher() { id<GREYMatcher> PasswordIconMatcher() {
return grey_accessibilityID( return grey_accessibilityID(
...@@ -41,6 +54,12 @@ id<GREYMatcher> PasswordTableViewMatcher() { ...@@ -41,6 +54,12 @@ id<GREYMatcher> PasswordTableViewMatcher() {
manual_fill::PasswordTableViewAccessibilityIdentifier); manual_fill::PasswordTableViewAccessibilityIdentifier);
} }
// Returns a matcher for the password search bar in manual fallback.
id<GREYMatcher> PasswordSearchBarMatcher() {
return grey_accessibilityID(
manual_fill::PasswordSearchBarAccessibilityIdentifier);
}
// Returns a matcher for the button to open password settings in manual // Returns a matcher for the button to open password settings in manual
// fallback. // fallback.
id<GREYMatcher> ManagePasswordsMatcher() { id<GREYMatcher> ManagePasswordsMatcher() {
...@@ -48,11 +67,124 @@ id<GREYMatcher> ManagePasswordsMatcher() { ...@@ -48,11 +67,124 @@ id<GREYMatcher> ManagePasswordsMatcher() {
manual_fill::ManagePasswordsAccessibilityIdentifier); manual_fill::ManagePasswordsAccessibilityIdentifier);
} }
// Returns a matcher for the button to open all passwords in manual fallback.
id<GREYMatcher> OtherPasswordsMatcher() {
return grey_accessibilityID(
manual_fill::OtherPasswordsAccessibilityIdentifier);
}
// Returns a matcher for the example username in the list.
id<GREYMatcher> UsernameButtonMatcher() {
return grey_buttonTitle(base::SysUTF8ToNSString(kExampleUsername));
}
// Returns a matcher for the password settings collection view. // Returns a matcher for the password settings collection view.
id<GREYMatcher> PasswordSettingsMatcher() { id<GREYMatcher> PasswordSettingsMatcher() {
return grey_accessibilityID(@"SavePasswordsCollectionViewController"); return grey_accessibilityID(@"SavePasswordsCollectionViewController");
} }
// Returns a matcher for the search bar in password settings.
id<GREYMatcher> PasswordSettingsSearchMatcher() {
return grey_accessibilityID(@"SettingsSearchCellTextField");
}
// Gets the current password store.
scoped_refptr<password_manager::PasswordStore> GetPasswordStore() {
// ServiceAccessType governs behaviour in Incognito: only modifications with
// EXPLICIT_ACCESS, which correspond to user's explicit gesture, succeed.
// This test does not deal with Incognito, and should not run in Incognito
// context. Therefore IMPLICIT_ACCESS is used to let the test fail if in
// Incognito context.
return IOSChromePasswordStoreFactory::GetForBrowserState(
chrome_test_util::GetOriginalBrowserState(),
ServiceAccessType::IMPLICIT_ACCESS);
}
// This class is used to obtain results from the PasswordStore and hence both
// check the success of store updates and ensure that store has finished
// processing.
class TestStoreConsumer : public password_manager::PasswordStoreConsumer {
public:
void OnGetPasswordStoreResults(
std::vector<std::unique_ptr<autofill::PasswordForm>> obtained) override {
obtained_ = std::move(obtained);
}
const std::vector<autofill::PasswordForm>& GetStoreResults() {
results_.clear();
ResetObtained();
GetPasswordStore()->GetAutofillableLogins(this);
bool responded = base::test::ios::WaitUntilConditionOrTimeout(1.0, ^bool {
return !AreObtainedReset();
});
GREYAssert(responded, @"Obtaining fillable items took too long.");
AppendObtainedToResults();
GetPasswordStore()->GetBlacklistLogins(this);
responded = base::test::ios::WaitUntilConditionOrTimeout(1.0, ^bool {
return !AreObtainedReset();
});
GREYAssert(responded, @"Obtaining blacklisted items took too long.");
AppendObtainedToResults();
return results_;
}
private:
// Puts |obtained_| in a known state not corresponding to any PasswordStore
// state.
void ResetObtained() {
obtained_.clear();
obtained_.emplace_back(nullptr);
}
// Returns true if |obtained_| are in the reset state.
bool AreObtainedReset() { return obtained_.size() == 1 && !obtained_[0]; }
void AppendObtainedToResults() {
for (const auto& source : obtained_) {
results_.emplace_back(*source);
}
ResetObtained();
}
// Temporary cache of obtained store results.
std::vector<std::unique_ptr<autofill::PasswordForm>> obtained_;
// Combination of fillable and blacklisted credentials from the store.
std::vector<autofill::PasswordForm> results_;
};
// Saves |form| to the password store and waits until the async processing is
// done.
void SaveToPasswordStore(const autofill::PasswordForm& form) {
GetPasswordStore()->AddLogin(form);
// Check the result and ensure PasswordStore processed this.
TestStoreConsumer consumer;
for (const auto& result : consumer.GetStoreResults()) {
if (result == form)
return;
}
GREYFail(@"Stored form was not found in the PasswordStore results.");
}
// Saves an example form in the store.
void SaveExamplePasswordForm() {
autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername);
example.password_value = base::ASCIIToUTF16(kExamplePassword);
example.origin = GURL("https://example.com");
example.signon_realm = example.origin.spec();
SaveToPasswordStore(example);
}
// Removes all credentials stored.
void ClearPasswordStore() {
GetPasswordStore()->RemoveLoginsCreatedBetween(base::Time(), base::Time(),
base::Closure());
TestStoreConsumer consumer;
GREYAssert(consumer.GetStoreResults().empty(),
@"PasswordStore was not cleared.");
}
} // namespace } // namespace
// Integration Tests for Mannual Fallback Passwords View Controller. // Integration Tests for Mannual Fallback Passwords View Controller.
...@@ -71,6 +203,11 @@ id<GREYMatcher> PasswordSettingsMatcher() { ...@@ -71,6 +203,11 @@ id<GREYMatcher> PasswordSettingsMatcher() {
[ChromeEarlGrey waitForWebViewContainingText:"hello!"]; [ChromeEarlGrey waitForWebViewContainingText:"hello!"];
} }
- (void)tearDown {
ClearPasswordStore();
[super tearDown];
}
// Test that the passwords view controller appears on screen. // Test that the passwords view controller appears on screen.
- (void)testPasswordsViewControllerIsPresented { - (void)testPasswordsViewControllerIsPresented {
// TODO:(https://crbug.com/878388) Enable on iPad when supported. // TODO:(https://crbug.com/878388) Enable on iPad when supported.
...@@ -133,4 +270,82 @@ id<GREYMatcher> PasswordSettingsMatcher() { ...@@ -133,4 +270,82 @@ id<GREYMatcher> PasswordSettingsMatcher() {
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
} }
// Test that the Password View Controller is not present when presenting UI.
- (void)testPasswordControllerPauses {
// TODO:(https://crbug.com/878388) Enable on iPad when supported.
if (IsIPadIdiom())
return;
// For the search bar to appear in password settings at least one password is
// needed.
SaveExamplePasswordForm();
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElement(kFormElementUsername)];
// Tap on the passwords icon.
[[EarlGrey selectElementWithMatcher:PasswordIconMatcher()]
performAction:grey_tap()];
// Tap the "Manage Passwords..." action.
[[EarlGrey selectElementWithMatcher:ManagePasswordsMatcher()]
performAction:grey_tap()];
// Tap the password search.
[[EarlGrey selectElementWithMatcher:PasswordSettingsSearchMatcher()]
performAction:grey_tap()];
// Verify keyboard is shown without the password controller.
GREYAssertTrue([GREYKeyboard isKeyboardShown], @"Keyboard Should be Shown");
[[EarlGrey selectElementWithMatcher:PasswordTableViewMatcher()]
assertWithMatcher:grey_notVisible()];
}
// Test that the Password View Controller is resumed after selecting other
// password.
- (void)testPasswordControllerResumes {
// TODO:(https://crbug.com/878388) Enable on iPad when supported.
if (IsIPadIdiom())
return;
// For this test one password is needed.
SaveExamplePasswordForm();
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElement(kFormElementUsername)];
// Tap on the passwords icon.
[[EarlGrey selectElementWithMatcher:PasswordIconMatcher()]
performAction:grey_tap()];
// Tap the "Manage Passwords..." action.
[[EarlGrey selectElementWithMatcher:OtherPasswordsMatcher()]
performAction:grey_tap()];
// Tap the password search.
[[EarlGrey selectElementWithMatcher:PasswordSearchBarMatcher()]
performAction:grey_tap()];
GREYAssertTrue([GREYKeyboard isKeyboardShown], @"Keyboard Should be Shown");
// Select a username.
[[EarlGrey selectElementWithMatcher:UsernameButtonMatcher()]
performAction:grey_tap()];
// Only on iOS 12 it is certain that on iPhones the keyboard is back. On iOS
// 11, it varies by device and version.
if (base::ios::IsRunningOnIOS12OrLater()) {
// Verify the password controller table view and the keyboard are visible.
GREYAssertTrue([GREYKeyboard isKeyboardShown], @"Keyboard Should be Shown");
[[EarlGrey selectElementWithMatcher:PasswordTableViewMatcher()]
assertWithMatcher:grey_sufficientlyVisible()];
} else if (!base::ios::IsRunningOnIOS11OrLater()) {
// On iOS 10 the keyboard is hidden.
GREYAssertFalse([GREYKeyboard isKeyboardShown],
@"Keyboard Should be Hidden");
}
}
@end @end
...@@ -243,46 +243,47 @@ ios_framework_bundle("earl_grey") { ...@@ -243,46 +243,47 @@ ios_framework_bundle("earl_grey") {
"src/EarlGrey/Traversal/GREYTraversalDFS.m", "src/EarlGrey/Traversal/GREYTraversalDFS.m",
] ]
public_headers = [ public_headers = [
"src/EarlGrey/EarlGrey.h",
"src/EarlGrey/Core/EarlGreyImpl.h",
"src/EarlGrey/Action/GREYAction.h", "src/EarlGrey/Action/GREYAction.h",
"src/EarlGrey/Action/GREYActionBlock.h", "src/EarlGrey/Action/GREYActionBlock.h",
"src/EarlGrey/Action/GREYActions.h", "src/EarlGrey/Action/GREYActions.h",
"src/EarlGrey/Matcher/GREYAllOf.h", "src/EarlGrey/Action/GREYBaseAction.h",
"src/EarlGrey/Matcher/GREYAnyOf.h", "src/EarlGrey/Action/GREYScrollActionError.h",
"src/EarlGrey/AppSupport/GREYIdlingResource.h",
"src/EarlGrey/Assertion/GREYAssertion.h", "src/EarlGrey/Assertion/GREYAssertion.h",
"src/EarlGrey/Assertion/GREYAssertionBlock.h", "src/EarlGrey/Assertion/GREYAssertionBlock.h",
"src/EarlGrey/Assertion/GREYAssertionDefines.h", "src/EarlGrey/Assertion/GREYAssertionDefines.h",
"src/EarlGrey/Assertion/GREYAssertions.h", "src/EarlGrey/Assertion/GREYAssertions.h",
"src/EarlGrey/Action/GREYBaseAction.h",
"src/EarlGrey/Matcher/GREYBaseMatcher.h",
"src/EarlGrey/Synchronization/GREYCondition.h",
"src/EarlGrey/Common/GREYConfiguration.h", "src/EarlGrey/Common/GREYConfiguration.h",
"src/EarlGrey/Common/GREYConstants.h", "src/EarlGrey/Common/GREYConstants.h",
"src/EarlGrey/Provider/GREYDataEnumerator.h",
"src/EarlGrey/Common/GREYDefines.h", "src/EarlGrey/Common/GREYDefines.h",
"src/EarlGrey/Matcher/GREYDescription.h",
"src/EarlGrey/Synchronization/GREYDispatchQueueIdlingResource.h",
"src/EarlGrey/Core/GREYElementFinder.h",
"src/EarlGrey/Common/GREYElementHierarchy.h", "src/EarlGrey/Common/GREYElementHierarchy.h",
"src/EarlGrey/Common/GREYScreenshotUtil.h",
"src/EarlGrey/Common/GREYTestHelper.h",
"src/EarlGrey/Core/EarlGreyImpl.h",
"src/EarlGrey/Core/GREYElementFinder.h",
"src/EarlGrey/Core/GREYElementInteraction.h", "src/EarlGrey/Core/GREYElementInteraction.h",
"src/EarlGrey/Matcher/GREYElementMatcherBlock.h", "src/EarlGrey/Core/GREYInteraction.h",
"src/EarlGrey/Core/GREYKeyboard.h",
"src/EarlGrey/EarlGrey.h",
"src/EarlGrey/Exception/GREYFailureHandler.h", "src/EarlGrey/Exception/GREYFailureHandler.h",
"src/EarlGrey/Exception/GREYFrameworkException.h", "src/EarlGrey/Exception/GREYFrameworkException.h",
"src/EarlGrey/AppSupport/GREYIdlingResource.h", "src/EarlGrey/Matcher/GREYAllOf.h",
"src/EarlGrey/Core/GREYInteraction.h", "src/EarlGrey/Matcher/GREYAnyOf.h",
"src/EarlGrey/Matcher/GREYBaseMatcher.h",
"src/EarlGrey/Matcher/GREYDescription.h",
"src/EarlGrey/Matcher/GREYElementMatcherBlock.h",
"src/EarlGrey/Matcher/GREYLayoutConstraint.h", "src/EarlGrey/Matcher/GREYLayoutConstraint.h",
"src/EarlGrey/Synchronization/GREYManagedObjectContextIdlingResource.h",
"src/EarlGrey/Matcher/GREYMatcher.h", "src/EarlGrey/Matcher/GREYMatcher.h",
"src/EarlGrey/Matcher/GREYMatchers.h", "src/EarlGrey/Matcher/GREYMatchers.h",
"src/EarlGrey/Synchronization/GREYNSTimerIdlingResource.h",
"src/EarlGrey/Matcher/GREYNot.h", "src/EarlGrey/Matcher/GREYNot.h",
"src/EarlGrey/Synchronization/GREYOperationQueueIdlingResource.h", "src/EarlGrey/Provider/GREYDataEnumerator.h",
"src/EarlGrey/Provider/GREYProvider.h", "src/EarlGrey/Provider/GREYProvider.h",
"src/EarlGrey/Common/GREYScreenshotUtil.h", "src/EarlGrey/Synchronization/GREYCondition.h",
"src/EarlGrey/Action/GREYScrollActionError.h", "src/EarlGrey/Synchronization/GREYDispatchQueueIdlingResource.h",
"src/EarlGrey/Synchronization/GREYManagedObjectContextIdlingResource.h",
"src/EarlGrey/Synchronization/GREYNSTimerIdlingResource.h",
"src/EarlGrey/Synchronization/GREYOperationQueueIdlingResource.h",
"src/EarlGrey/Synchronization/GREYSyncAPI.h", "src/EarlGrey/Synchronization/GREYSyncAPI.h",
"src/EarlGrey/Common/GREYTestHelper.h",
"src/EarlGrey/Synchronization/GREYUIThreadExecutor.h", "src/EarlGrey/Synchronization/GREYUIThreadExecutor.h",
] ]
deps = [ deps = [
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment