blob: c2021048ec8ca4a8de20137a603bbe5adb16c702 [file] [log] [blame]
// Copyright 2017 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.
#include "ash/login/ui/login_password_view.h"
#include "ash/login/ui/login_test_base.h"
#include "ash/public/cpp/login_types.h"
#include "ash/shell.h"
#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/mock_timer.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
class LoginPasswordViewTest : public LoginTestBase {
protected:
LoginPasswordViewTest() = default;
~LoginPasswordViewTest() override = default;
// LoginScreenTest:
void SetUp() override {
LoginTestBase::SetUp();
view_ = new LoginPasswordView();
view_->Init(
base::BindRepeating(&LoginPasswordViewTest::OnPasswordSubmit,
base::Unretained(this)),
base::BindRepeating(&LoginPasswordViewTest::OnPasswordTextChanged,
base::Unretained(this)),
base::BindRepeating(&LoginPasswordViewTest::OnEasyUnlockIconHovered,
base::Unretained(this)),
base::BindRepeating(&LoginPasswordViewTest::OnEasyUnlockIconTapped,
base::Unretained(this)));
SetWidget(CreateWidgetWithContent(view_));
}
void OnPasswordSubmit(const base::string16& password) {
password_ = password;
}
void OnPasswordTextChanged(bool is_empty) {
is_password_field_empty_ = is_empty;
}
void OnEasyUnlockIconHovered() { easy_unlock_icon_hovered_called_ = true; }
void OnEasyUnlockIconTapped() { easy_unlock_icon_tapped_called_ = true; }
LoginPasswordView* view_ = nullptr;
base::Optional<base::string16> password_;
bool is_password_field_empty_ = true;
bool easy_unlock_icon_hovered_called_ = false;
bool easy_unlock_icon_tapped_called_ = false;
private:
DISALLOW_COPY_AND_ASSIGN(LoginPasswordViewTest);
};
// LoginPasswordViewTest with display password button feature enabled.
class LoginPasswordViewTestFeatureEnabled : public LoginPasswordViewTest {
protected:
LoginPasswordViewTestFeatureEnabled() {
feature_list_.InitWithFeatures(
{chromeos::features::kLoginDisplayPasswordButton}, {});
}
LoginPasswordViewTestFeatureEnabled(
const LoginPasswordViewTestFeatureEnabled&) = delete;
LoginPasswordViewTestFeatureEnabled& operator=(
const LoginPasswordViewTestFeatureEnabled&) = delete;
~LoginPasswordViewTestFeatureEnabled() override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
} // namespace
// Verifies that the submit button updates its UI state.
TEST_F(LoginPasswordViewTest, SubmitButtonUpdatesUiState) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// The submit button starts with the disabled state.
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.submit_button()->GetEnabled());
// Enter 'a'. The submit button is enabled.
generator->PressKey(ui::KeyboardCode::VKEY_A, 0);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.submit_button()->GetEnabled());
// Enter 'b'. The submit button stays enabled.
generator->PressKey(ui::KeyboardCode::VKEY_B, 0);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.submit_button()->GetEnabled());
// Clear password. The submit button is disabled.
view_->Clear();
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.submit_button()->GetEnabled());
// Enter 'a'. The submit button is enabled.
generator->PressKey(ui::KeyboardCode::VKEY_A, 0);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.submit_button()->GetEnabled());
// Set the text field to be read-only. The submit button is disabled.
view_->SetReadOnly(true);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_FALSE(test_api.submit_button()->GetEnabled());
// Set the text field to be not read-only. The submit button is enabled.
view_->SetReadOnly(false);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.submit_button()->GetEnabled());
}
// Verifies that the display password button updates its UI state.
TEST_F(LoginPasswordViewTestFeatureEnabled,
DisplayPasswordButtonUpdatesUiState) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// The display password button is not toggled by default and the password is
// not visible.
EXPECT_FALSE(test_api.display_password_button()->toggled_for_testing());
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
// Click the display password button. This should not work as long as the
// password textfield is empty.
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_FALSE(test_api.display_password_button()->toggled_for_testing());
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
// Click the display password button. The password should be visible.
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_TRUE(test_api.display_password_button()->toggled_for_testing());
EXPECT_EQ(test_api.textfield()->GetTextInputType(), ui::TEXT_INPUT_TYPE_NULL);
// Click the display password button again. The password should be hidden.
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_FALSE(test_api.display_password_button()->toggled_for_testing());
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
}
// Verifies that password submit works with 'Enter'.
TEST_F(LoginPasswordViewTest, PasswordSubmitIncludesPasswordText) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
generator->PressKey(ui::KeyboardCode::VKEY_A, 0);
generator->PressKey(ui::KeyboardCode::VKEY_B, 0);
generator->PressKey(ui::KeyboardCode::VKEY_C, 0);
generator->PressKey(ui::KeyboardCode::VKEY_1, 0);
generator->PressKey(ui::KeyboardCode::VKEY_RETURN, 0);
ASSERT_TRUE(password_.has_value());
EXPECT_EQ(base::ASCIIToUTF16("abc1"), *password_);
}
// Verifies that password submit works when clicking the submit button.
TEST_F(LoginPasswordViewTest, PasswordSubmitViaButton) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
generator->PressKey(ui::KeyboardCode::VKEY_A, 0);
generator->PressKey(ui::KeyboardCode::VKEY_B, 0);
generator->PressKey(ui::KeyboardCode::VKEY_C, 0);
generator->PressKey(ui::KeyboardCode::VKEY_1, 0);
generator->MoveMouseTo(
test_api.submit_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
ASSERT_TRUE(password_.has_value());
EXPECT_EQ(base::ASCIIToUTF16("abc1"), *password_);
}
// Verifies that text is not cleared after submitting a password.
TEST_F(LoginPasswordViewTest, PasswordSubmitClearsPassword) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Submit 'a' password.
EXPECT_TRUE(is_password_field_empty_);
generator->PressKey(ui::KeyboardCode::VKEY_A, 0);
EXPECT_FALSE(is_password_field_empty_);
generator->PressKey(ui::KeyboardCode::VKEY_RETURN, 0);
EXPECT_FALSE(is_password_field_empty_);
ASSERT_TRUE(password_.has_value());
EXPECT_EQ(base::ASCIIToUTF16("a"), *password_);
// Clear password.
password_.reset();
view_->Clear();
EXPECT_TRUE(is_password_field_empty_);
// Submit 'b' password.
generator->PressKey(ui::KeyboardCode::VKEY_B, 0);
EXPECT_FALSE(is_password_field_empty_);
generator->PressKey(ui::KeyboardCode::VKEY_RETURN, 0);
EXPECT_FALSE(is_password_field_empty_);
ASSERT_TRUE(password_.has_value());
// The submitted password is 'b' instead of "ab".
EXPECT_EQ(base::ASCIIToUTF16("b"), *password_);
}
// Verifies that clicking the easy unlock icon fires the click event.
TEST_F(LoginPasswordViewTest, EasyUnlockClickFiresEvent) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Enable icon.
view_->SetEasyUnlockIcon(EasyUnlockIconId::SPINNER,
base::string16() /*accessibility_label*/);
ASSERT_TRUE(test_api.easy_unlock_icon()->GetVisible());
// Click to the right of the icon, call is not generated.
EXPECT_FALSE(easy_unlock_icon_tapped_called_);
generator->MoveMouseTo(
test_api.easy_unlock_icon()->GetBoundsInScreen().bottom_right() +
gfx::Vector2d(2, 0));
generator->ClickLeftButton();
EXPECT_FALSE(easy_unlock_icon_tapped_called_);
// Click the icon.
EXPECT_FALSE(easy_unlock_icon_tapped_called_);
generator->MoveMouseTo(
test_api.easy_unlock_icon()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_TRUE(easy_unlock_icon_tapped_called_);
// Icon was not hovered (since we did not enable immediate hover).
EXPECT_FALSE(easy_unlock_icon_hovered_called_);
}
// Verifies that hovering the icon fires the hover event.
TEST_F(LoginPasswordViewTest, EasyUnlockMouseHover) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Enable icon, enable immediate hovering.
view_->SetEasyUnlockIcon(EasyUnlockIconId::SPINNER,
base::string16() /*accessibility_label*/);
test_api.set_immediately_hover_easy_unlock_icon();
ASSERT_TRUE(test_api.easy_unlock_icon()->GetVisible());
// Hover over the icon.
EXPECT_FALSE(easy_unlock_icon_hovered_called_);
generator->MoveMouseTo(
test_api.easy_unlock_icon()->GetBoundsInScreen().CenterPoint());
EXPECT_TRUE(easy_unlock_icon_hovered_called_);
// Icon was not tapped.
EXPECT_FALSE(easy_unlock_icon_tapped_called_);
}
// Checks that the user can't hit Ctrl+Z to revert the password when it has been
// cleared.
TEST_F(LoginPasswordViewTest, CtrlZDisabled) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_FALSE(is_password_field_empty_);
view_->Clear();
EXPECT_TRUE(is_password_field_empty_);
generator->PressKey(ui::KeyboardCode::VKEY_Z, ui::EF_CONTROL_DOWN);
EXPECT_TRUE(is_password_field_empty_);
}
// Verifies that the password textfield clears after a delay when the display
// password button is shown.
TEST_F(LoginPasswordViewTestFeatureEnabled, PasswordAutoClearsAndHides) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Install mock timers into the password view.
auto clear_timer0 = std::make_unique<base::MockRetainingOneShotTimer>();
auto hide_timer0 = std::make_unique<base::MockRetainingOneShotTimer>();
base::MockRetainingOneShotTimer* clear_timer = clear_timer0.get();
base::MockRetainingOneShotTimer* hide_timer = hide_timer0.get();
test_api.SetTimers(std::move(clear_timer0), std::move(hide_timer0));
// Verify clearing timer works.
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_FALSE(is_password_field_empty_);
clear_timer->Fire();
EXPECT_TRUE(is_password_field_empty_);
// Check a second time.
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_FALSE(is_password_field_empty_);
clear_timer->Fire();
EXPECT_TRUE(is_password_field_empty_);
// Verify hiding timer works; set the password visible first then fire the
// hiding timer and check it is hidden.
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_EQ(test_api.textfield()->GetTextInputType(), ui::TEXT_INPUT_TYPE_NULL);
hide_timer->Fire();
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
// Hide an empty password already hidden and make sure a second fire works.
hide_timer->Fire();
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
}
// Verifies that the password textfield remains in the same visibility state
// when the content changes.
TEST_F(LoginPasswordViewTestFeatureEnabled,
ContentChangesDoNotImpactPasswordVisibility) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Type to enable the display password button and click on it to display the
// password.
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_EQ(test_api.textfield()->GetTextInputType(), ui::TEXT_INPUT_TYPE_NULL);
// Type manually and programmatically, and check if the password textfield
// remains visible.
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_EQ(test_api.textfield()->GetTextInputType(), ui::TEXT_INPUT_TYPE_NULL);
test_api.textfield()->InsertText(base::ASCIIToUTF16("test"));
EXPECT_EQ(test_api.textfield()->GetTextInputType(), ui::TEXT_INPUT_TYPE_NULL);
// Click again on the display password button to hide the password.
generator->MoveMouseTo(
test_api.display_password_button()->GetBoundsInScreen().CenterPoint());
generator->ClickLeftButton();
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
// Type manually and programmatically, and check if the password textfield
// remains invisible.
test_api.textfield()->InsertText(base::ASCIIToUTF16("test"));
EXPECT_EQ(test_api.textfield()->GetTextInputType(),
ui::TEXT_INPUT_TYPE_PASSWORD);
}
// Checks that the display password button is disabled when the textfield is
// empty and enabled when it is not.
TEST_F(LoginPasswordViewTestFeatureEnabled,
DisplayPasswordButonIsEnabledIFFTextfieldIsNotEmpty) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.display_password_button()->GetEnabled());
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.display_password_button()->GetEnabled());
generator->PressKey(ui::KeyboardCode::VKEY_BACK, ui::EF_NONE);
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.display_password_button()->GetEnabled());
test_api.textfield()->InsertText(base::ASCIIToUTF16("test"));
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.display_password_button()->GetEnabled());
view_->Clear();
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.display_password_button()->GetEnabled());
generator->PressKey(ui::KeyboardCode::VKEY_A, ui::EF_NONE);
EXPECT_FALSE(is_password_field_empty_);
EXPECT_TRUE(test_api.display_password_button()->GetEnabled());
test_api.textfield()->SelectAll(false /* reversed */);
generator->PressKey(ui::KeyboardCode::VKEY_DELETE, ui::EF_NONE);
EXPECT_TRUE(is_password_field_empty_);
EXPECT_FALSE(test_api.display_password_button()->GetEnabled());
}
// Verifies that focus returned to the textfield after InsertNumber is called.
TEST_F(LoginPasswordViewTestFeatureEnabled, FocusReturn) {
LoginPasswordView::TestApi test_api(view_);
ui::test::EventGenerator* generator = GetEventGenerator();
// Verify that focus is returned to view after the number insertion.
view_->InsertNumber(0);
EXPECT_TRUE(test_api.textfield()->HasFocus());
// Focus on the next element to check that following focus return will not
// delete what was already inserted into textfield.
generator->PressKey(ui::KeyboardCode::VKEY_TAB, ui::EventFlags::EF_NONE);
EXPECT_FALSE(test_api.textfield()->HasFocus());
view_->InsertNumber(1);
EXPECT_TRUE(test_api.textfield()->HasFocus());
EXPECT_EQ(test_api.textfield()->GetText().length(), 2u);
}
} // namespace ash