Commit 45b4df18 authored by dconnelly's avatar dconnelly Committed by Commit bot

Implement CredentialItemView and tests for the Mac account chooser.

Depends on https://codereview.chromium.org/901493003/

BUG=448011

Review URL: https://codereview.chromium.org/878743007

Cr-Commit-Position: refs/heads/master@{#319084}
parent d6e13ee1
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_UI_COCOA_PASSWORDS_CREDENTIAL_ITEM_VIEW_H_
#define CHROME_BROWSER_UI_COCOA_PASSWORDS_CREDENTIAL_ITEM_VIEW_H_
#import <Cocoa/Cocoa.h>
#import "base/mac/scoped_nsobject.h"
#include "components/autofill/core/common/password_form.h"
#include "components/password_manager/content/common/credential_manager_types.h"
@class CredentialItemView;
class GURL;
// Handles user interaction with and image fetching for a CredentialItemView.
@protocol CredentialItemDelegate<NSObject>
// Retrieves the image located at |avatarURL| and updates |view| by calling
// [CredentialItemView updateAvatar:] if successful.
- (void)fetchAvatar:(const GURL&)avatarURL forView:(CredentialItemView*)view;
@end
// A view to show a single account credential.
@interface CredentialItemView : NSView {
autofill::PasswordForm passwordForm_;
password_manager::CredentialType credentialType_;
NSTextField* nameLabel_;
NSTextField* usernameLabel_;
NSImageView* avatarView_;
id<CredentialItemDelegate> delegate_; // Weak.
}
@property(nonatomic, readonly) autofill::PasswordForm passwordForm;
@property(nonatomic, readonly) password_manager::CredentialType credentialType;
// Initializes an item view populated with the data in |passwordForm|. Uses
// |delegate| to asynchronously fetch the avatar image.
- (id)initWithPasswordForm:(const autofill::PasswordForm&)passwordForm
credentialType:(password_manager::CredentialType)credentialType
delegate:(id<CredentialItemDelegate>)delegate;
// Sets a custom avatar for this item. The image should be scaled and cropped
// to a circle of size |kAvatarImageSize|, otherwise it will look ridiculous.
- (void)updateAvatar:(NSImage*)avatar;
// The default avatar image, used when a custom one is not set.
+ (NSImage*)defaultAvatar;
@end
#endif // CHROME_BROWSER_UI_COCOA_PASSWORDS_CREDENTIAL_ITEM_VIEW_H_
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/passwords/credential_item_view.h"
#include <algorithm>
#include "base/i18n/rtl.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/ui/passwords/manage_passwords_bubble_model.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
#include "grit/theme_resources.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_util_mac.h"
namespace {
const CGFloat kHorizontalPaddingBetweenAvatarAndLabels = 10.0f;
const CGFloat kVerticalPaddingBetweenLabels = 2.0f;
} // namespace
@interface CredentialItemView()
@property(nonatomic, readonly) NSTextField* nameLabel;
@property(nonatomic, readonly) NSTextField* usernameLabel;
@property(nonatomic, readonly) NSImageView* avatarView;
@end
@implementation CredentialItemView
@synthesize nameLabel = nameLabel_;
@synthesize usernameLabel = usernameLabel_;
@synthesize avatarView = avatarView_;
@synthesize passwordForm = passwordForm_;
@synthesize credentialType = credentialType_;
- (id)initWithPasswordForm:(const autofill::PasswordForm&)passwordForm
credentialType:(password_manager::CredentialType)credentialType
delegate:(id<CredentialItemDelegate>)delegate {
if ((self = [super init])) {
passwordForm_ = passwordForm;
credentialType_ = credentialType;
delegate_ = delegate;
// -----------------------------------------------
// | | John Q. Facebooker |
// | icon | john@somewhere.com |
// -----------------------------------------------
// Create the views.
avatarView_ = [[[NSImageView alloc] initWithFrame:NSZeroRect] autorelease];
[avatarView_ setWantsLayer:YES];
[[avatarView_ layer] setCornerRadius:kAvatarImageSize / 2.0f];
[[avatarView_ layer] setMasksToBounds:YES];
[self addSubview:avatarView_];
if (!passwordForm_.display_name.empty()) {
nameLabel_ = [[[NSTextField alloc] initWithFrame:NSZeroRect] autorelease];
[self addSubview:nameLabel_];
[nameLabel_ setBezeled:NO];
[nameLabel_ setDrawsBackground:NO];
[nameLabel_ setEditable:NO];
[nameLabel_ setSelectable:NO];
[nameLabel_
setStringValue:base::SysUTF16ToNSString(passwordForm_.display_name)];
[nameLabel_ setAlignment:base::i18n::IsRTL() ? NSRightTextAlignment
: NSLeftTextAlignment];
[nameLabel_ sizeToFit];
}
usernameLabel_ =
[[[NSTextField alloc] initWithFrame:NSZeroRect] autorelease];
[self addSubview:usernameLabel_];
[usernameLabel_ setBezeled:NO];
[usernameLabel_ setDrawsBackground:NO];
[usernameLabel_ setEditable:NO];
[usernameLabel_ setSelectable:NO];
[usernameLabel_
setStringValue:base::SysUTF16ToNSString(passwordForm_.username_value)];
[usernameLabel_ setAlignment:base::i18n::IsRTL() ? NSRightTextAlignment
: NSLeftTextAlignment];
[usernameLabel_ sizeToFit];
// Compute the heights and widths of everything, as the layout depends on
// these measurements.
const CGFloat labelsHeight = NSHeight([nameLabel_ frame]) +
NSHeight([usernameLabel_ frame]) +
kVerticalPaddingBetweenLabels;
const CGFloat height = std::max(labelsHeight, CGFloat(kAvatarImageSize));
const CGFloat width =
kAvatarImageSize + kHorizontalPaddingBetweenAvatarAndLabels +
std::max(NSWidth([nameLabel_ frame]), NSWidth([usernameLabel_ frame]));
self.frame = NSMakeRect(0, 0, width, height);
// Lay out the views (RTL reverses the order horizontally).
const CGFloat avatarX = base::i18n::IsRTL() ? width - kAvatarImageSize : 0;
const CGFloat avatarY =
(kAvatarImageSize > height) ? 0 : (height - kAvatarImageSize) / 2.0f;
[avatarView_ setFrame:NSMakeRect(avatarX, avatarY, kAvatarImageSize,
kAvatarImageSize)];
const CGFloat usernameX =
base::i18n::IsRTL()
? NSMinX([avatarView_ frame]) -
kHorizontalPaddingBetweenAvatarAndLabels -
NSWidth([usernameLabel_ frame])
: NSMaxX([avatarView_ frame]) +
kHorizontalPaddingBetweenAvatarAndLabels;
const CGFloat usernameLabelY =
(labelsHeight > height) ? 0 : (height - labelsHeight) / 2.0f;
NSRect usernameFrame = [usernameLabel_ frame];
usernameFrame.origin = NSMakePoint(usernameX, usernameLabelY);
[usernameLabel_ setFrame:usernameFrame];
const CGFloat nameX = base::i18n::IsRTL()
? NSMinX([avatarView_ frame]) -
kHorizontalPaddingBetweenAvatarAndLabels -
NSWidth([nameLabel_ frame])
: NSMaxX([avatarView_ frame]) +
kHorizontalPaddingBetweenAvatarAndLabels;
const CGFloat nameLabelY =
NSMaxY(usernameFrame) + kVerticalPaddingBetweenLabels;
NSRect nameFrame = [nameLabel_ frame];
nameFrame.origin = NSMakePoint(nameX, nameLabelY);
[nameLabel_ setFrame:nameFrame];
// Use a default avatar and fetch the custom one, if it exists.
[self updateAvatar:[[self class] defaultAvatar]];
if (passwordForm_.avatar_url.is_valid())
[delegate_ fetchAvatar:passwordForm_.avatar_url forView:self];
// When resizing, stick to the left (resp. right for RTL) edge.
const NSUInteger autoresizingMask =
(base::i18n::IsRTL() ? NSViewMinXMargin : NSViewMaxXMargin);
[avatarView_ setAutoresizingMask:autoresizingMask];
[usernameLabel_ setAutoresizingMask:autoresizingMask];
[nameLabel_ setAutoresizingMask:autoresizingMask];
[self setAutoresizingMask:NSViewWidthSizable];
}
return self;
}
- (void)updateAvatar:(NSImage*)avatar {
[avatarView_ setImage:avatar];
}
+ (NSImage*)defaultAvatar {
return gfx::NSImageFromImageSkia(ScaleImageForAccountAvatar(
*ResourceBundle::GetSharedInstance()
.GetImageNamed(IDR_PROFILE_AVATAR_PLACEHOLDER_LARGE)
.ToImageSkia()));
}
@end
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/passwords/credential_item_view.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
#include "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "third_party/ocmock/gtest_support.h"
#include "ui/gfx/image/image.h"
@interface CredentialItemView(Testing)
@property(nonatomic, readonly) NSTextField* nameLabel;
@property(nonatomic, readonly) NSTextField* usernameLabel;
@property(nonatomic, readonly) NSImageView* avatarView;
@end
// A test implementation of a CredentialItemDelegate to stub out interactions.
@interface CredentialItemTestDelegate : NSObject<CredentialItemDelegate>
@property(nonatomic, readonly) BOOL didFetchAvatar;
@property(nonatomic, readonly) GURL fetchedAvatarURL;
@property(nonatomic, readonly) CredentialItemView* viewForFetchedAvatar;
@property(nonatomic, readonly) BOOL didSelectPasswordForm;
@property(nonatomic, readonly) autofill::PasswordForm selectedPasswordForm;
@property(nonatomic, readonly)
password_manager::CredentialType selectedCredentialType;
@end
@implementation CredentialItemTestDelegate
@synthesize didFetchAvatar = didFetchAvatar_;
@synthesize fetchedAvatarURL = fetchedAvatarURL_;
@synthesize viewForFetchedAvatar = viewForFetchedAvatar_;
@synthesize didSelectPasswordForm = didSelectPasswordForm_;
@synthesize selectedPasswordForm = selectedPasswordForm_;
@synthesize selectedCredentialType = selectedCredentialType_;
- (void)fetchAvatar:(const GURL&)avatarURL forView:(CredentialItemView*)view {
didFetchAvatar_ = YES;
fetchedAvatarURL_ = avatarURL;
viewForFetchedAvatar_ = view;
}
- (void)selectPasswordForm:(const autofill::PasswordForm&)passwordForm
credentialType:(password_manager::CredentialType)credentialType {
didSelectPasswordForm_ = YES;
selectedPasswordForm_ = passwordForm;
selectedCredentialType_ = credentialType;
}
@end
namespace {
// Determines whether |left| and |right| have the same data representation.
// Necessary because [CredentialItemView defaultAvatar] does some ImageSkia
// stuff that creates new NSImage instances.
bool ImagesEqual(NSImage* left, NSImage* right) {
if (!left || !right)
return left == right;
gfx::Image leftImage([left copy]);
gfx::Image rightImage([right copy]);
return leftImage.As1xPNGBytes()->Equals(rightImage.As1xPNGBytes());
}
// Returns a PasswordForm with only a username.
autofill::PasswordForm BasicCredential() {
autofill::PasswordForm credential;
credential.username_value = base::ASCIIToUTF16("taco");
return credential;
}
// Returns a PasswordForm with a username and display name.
autofill::PasswordForm CredentialWithName() {
autofill::PasswordForm credential;
credential.username_value = base::ASCIIToUTF16("pizza");
credential.display_name = base::ASCIIToUTF16("margherita pizza");
return credential;
}
// Returns a PasswordForm with a username and avatar URL.
autofill::PasswordForm CredentialWithAvatar() {
autofill::PasswordForm credential;
credential.username_value = base::ASCIIToUTF16("sandwich");
credential.avatar_url = GURL("http://sandwich.com/pastrami.jpg");
return credential;
}
// Returns a PasswordForm with a username, display name, and avatar URL.
autofill::PasswordForm CredentialWithNameAndAvatar() {
autofill::PasswordForm credential;
credential.username_value = base::ASCIIToUTF16("noodle");
credential.display_name = base::ASCIIToUTF16("pasta amatriciana");
credential.avatar_url = GURL("http://pasta.com/amatriciana.png");
return credential;
}
// Tests for CredentialItemViewTest.
class CredentialItemViewTest : public CocoaTest {
protected:
void SetUp() override {
delegate_.reset([[CredentialItemTestDelegate alloc] init]);
}
// Returns a delegate for testing.
CredentialItemTestDelegate* delegate() { return delegate_.get(); }
// Returns an autoreleased view populated from |form|.
CredentialItemView* view(const autofill::PasswordForm& form) {
return [[[CredentialItemView alloc]
initWithPasswordForm:form
credentialType:password_manager::CredentialType::
CREDENTIAL_TYPE_LOCAL
delegate:delegate()] autorelease];
}
private:
base::scoped_nsobject<CredentialItemTestDelegate> delegate_;
};
TEST_F(CredentialItemViewTest, BasicCredential) {
autofill::PasswordForm form(BasicCredential());
CredentialItemView* item = view(form);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.username_value),
[item usernameLabel].stringValue);
EXPECT_EQ(nil, [item nameLabel]);
EXPECT_FALSE([delegate() didFetchAvatar]);
EXPECT_TRUE(
ImagesEqual([CredentialItemView defaultAvatar], [item avatarView].image));
}
TEST_F(CredentialItemViewTest, CredentialWithName) {
autofill::PasswordForm form(CredentialWithName());
CredentialItemView* item = view(form);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.username_value),
[item usernameLabel].stringValue);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.display_name),
[item nameLabel].stringValue);
EXPECT_FALSE([delegate() didFetchAvatar]);
EXPECT_TRUE(
ImagesEqual([CredentialItemView defaultAvatar], [item avatarView].image));
}
TEST_F(CredentialItemViewTest, CredentialWithAvatar) {
autofill::PasswordForm form(CredentialWithAvatar());
CredentialItemView* item = view(form);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.username_value),
[item usernameLabel].stringValue);
EXPECT_EQ(nil, [item nameLabel]);
EXPECT_TRUE([delegate() didFetchAvatar]);
EXPECT_EQ(form.avatar_url, [delegate() fetchedAvatarURL]);
EXPECT_EQ(item, [delegate() viewForFetchedAvatar]);
EXPECT_TRUE(
ImagesEqual([CredentialItemView defaultAvatar], [item avatarView].image));
[item updateAvatar:nil];
EXPECT_FALSE([item avatarView].image);
}
TEST_F(CredentialItemViewTest, CredentialWithNameAndAvatar) {
autofill::PasswordForm form(CredentialWithNameAndAvatar());
CredentialItemView* item = view(form);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.username_value),
[item usernameLabel].stringValue);
EXPECT_NSEQ(base::SysUTF16ToNSString(form.display_name),
[item nameLabel].stringValue);
EXPECT_TRUE([delegate() didFetchAvatar]);
EXPECT_EQ(form.avatar_url, [delegate() fetchedAvatarURL]);
EXPECT_EQ(item, [delegate() viewForFetchedAvatar]);
EXPECT_TRUE(
ImagesEqual([CredentialItemView defaultAvatar], [item avatarView].image));
[item updateAvatar:nil];
EXPECT_FALSE([item avatarView].image);
}
} // namespace
......@@ -599,6 +599,8 @@
'browser/ui/cocoa/panels/panel_utils_cocoa.mm',
'browser/ui/cocoa/panels/panel_window_controller_cocoa.h',
'browser/ui/cocoa/panels/panel_window_controller_cocoa.mm',
'browser/ui/cocoa/passwords/credential_item_view.h',
'browser/ui/cocoa/passwords/credential_item_view.mm',
'browser/ui/cocoa/passwords/manage_password_item_view_controller.h',
'browser/ui/cocoa/passwords/manage_password_item_view_controller.mm',
'browser/ui/cocoa/passwords/manage_passwords_bubble_blacklist_view_controller.h',
......
......@@ -459,6 +459,7 @@
'browser/ui/cocoa/omnibox/omnibox_popup_view_mac_unittest.mm',
'browser/ui/cocoa/omnibox/omnibox_view_mac_unittest.mm',
'browser/ui/cocoa/panels/panel_cocoa_unittest.mm',
'browser/ui/cocoa/passwords/credential_item_view_unittest.mm',
'browser/ui/cocoa/passwords/manage_password_item_view_controller_unittest.mm',
'browser/ui/cocoa/passwords/manage_passwords_bubble_blacklist_view_controller_unittest.mm',
'browser/ui/cocoa/passwords/manage_passwords_bubble_cocoa_unittest.mm',
......
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