blob: bcb5723f15d1b49cb24a3078222045916f482b5d [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 <memory>
#include "ash/login/login_screen_controller.h"
#include "ash/login/mock_login_screen_client.h"
#include "ash/login/ui/fake_login_detachable_base_model.h"
#include "ash/login/ui/lock_contents_view.h"
#include "ash/login/ui/login_test_base.h"
#include "ash/login/ui/login_test_utils.h"
#include "ash/public/cpp/config.h"
#include "ash/root_window_controller.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/system/tray/system_tray.h"
#include "base/run_loop.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/widget/widget.h"
using ::testing::_;
using ::testing::Invoke;
using LockScreenSanityTest = ash::LoginTestBase;
namespace ash {
namespace {
class LockScreenAppFocuser {
public:
explicit LockScreenAppFocuser(views::Widget* lock_screen_app_widget)
: lock_screen_app_widget_(lock_screen_app_widget) {}
~LockScreenAppFocuser() = default;
bool reversed_tab_order() const { return reversed_tab_order_; }
void FocusLockScreenApp(bool reverse) {
reversed_tab_order_ = reverse;
lock_screen_app_widget_->Activate();
}
private:
bool reversed_tab_order_ = false;
views::Widget* lock_screen_app_widget_;
DISALLOW_COPY_AND_ASSIGN(LockScreenAppFocuser);
};
// Returns true if |view| or any child of it has focus.
bool HasFocusInAnyChildView(views::View* view) {
if (view->HasFocus())
return true;
for (int i = 0; i < view->child_count(); ++i) {
if (HasFocusInAnyChildView(view->child_at(i)))
return true;
}
return false;
}
// Keeps tabbing through |view| until the view loses focus.
// The number of generated tab events will be limited - if the focus is still
// within the view by the time the limit is hit, this will return false.
bool TabThroughView(ui::test::EventGenerator* event_generator,
views::View* view,
bool reverse) {
if (!HasFocusInAnyChildView(view)) {
ADD_FAILURE() << "View not focused initially.";
return false;
}
for (int i = 0; i < 50; ++i) {
event_generator->PressKey(ui::KeyboardCode::VKEY_TAB,
reverse ? ui::EF_SHIFT_DOWN : 0);
if (!HasFocusInAnyChildView(view))
return true;
}
return false;
}
testing::AssertionResult VerifyFocused(views::View* view) {
if (!view->GetWidget()->IsActive())
return testing::AssertionFailure() << "Widget not active.";
if (!HasFocusInAnyChildView(view))
return testing::AssertionFailure() << "No focused descendant.";
return testing::AssertionSuccess();
}
testing::AssertionResult VerifyNotFocused(views::View* view) {
if (view->GetWidget()->IsActive())
return testing::AssertionFailure() << "Widget active";
if (HasFocusInAnyChildView(view))
return testing::AssertionFailure() << "Has focused descendant.";
return testing::AssertionSuccess();
}
} // namespace
// Verifies that the password input box has focus.
TEST_F(LockScreenSanityTest, PasswordIsInitiallyFocused) {
// Build lock screen.
auto* contents = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
// The lock screen requires at least one user.
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
// Textfield should have focus.
EXPECT_EQ(MakeLoginPasswordTestApi(contents).textfield(),
contents->GetFocusManager()->GetFocusedView());
}
// Verifies submitting the password invokes mojo lock screen client.
TEST_F(LockScreenSanityTest, PasswordSubmitCallsLoginScreenClient) {
// Build lock screen.
auto* contents = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
// The lock screen requires at least one user.
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
// Password submit runs mojo.
std::unique_ptr<MockLoginScreenClient> client = BindMockLoginScreenClient();
client->set_authenticate_user_callback_result(false);
EXPECT_CALL(*client,
AuthenticateUser_(users()[0]->basic_user_info->account_id, _, _,
false, _));
ui::test::EventGenerator& generator = GetEventGenerator();
generator.PressKey(ui::KeyboardCode::VKEY_A, 0);
generator.PressKey(ui::KeyboardCode::VKEY_RETURN, 0);
base::RunLoop().RunUntilIdle();
}
// Verifies that password text is cleared only after the browser-process
// authentication request is complete and the auth fails.
TEST_F(LockScreenSanityTest,
PasswordSubmitClearsPasswordAfterFailedAuthentication) {
std::unique_ptr<MockLoginScreenClient> client = BindMockLoginScreenClient();
auto* contents = new LockContentsView(
mojom::TrayActionState::kAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(contents);
LoginPasswordView::TestApi password_test_api =
MakeLoginPasswordTestApi(contents);
MockLoginScreenClient::AuthenticateUserCallback callback;
auto submit_password = [&]() {
// Capture the authentication callback.
client->set_authenticate_user_callback_storage(&callback);
EXPECT_CALL(*client, AuthenticateUser_(testing::_, testing::_, testing::_,
testing::_, testing::_));
// Submit password with content 'a'. This creates a browser-process
// authentication request stored in |callback|.
DCHECK(callback.is_null());
ui::test::EventGenerator& generator = GetEventGenerator();
generator.PressKey(ui::KeyboardCode::VKEY_A, 0);
generator.PressKey(ui::KeyboardCode::VKEY_RETURN, 0);
base::RunLoop().RunUntilIdle();
DCHECK(!callback.is_null());
};
// Run the browser-process authentication request. Verify that the password is
// cleared after the ash callback handler has completed and auth has failed.
submit_password();
EXPECT_FALSE(password_test_api.textfield()->text().empty());
EXPECT_TRUE(password_test_api.textfield()->read_only());
std::move(callback).Run(false);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(password_test_api.textfield()->text().empty());
EXPECT_FALSE(password_test_api.textfield()->read_only());
// Repeat the above process. Verify that the password is not cleared if auth
// succeeds.
submit_password();
EXPECT_FALSE(password_test_api.textfield()->text().empty());
EXPECT_TRUE(password_test_api.textfield()->read_only());
std::move(callback).Run(true);
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(password_test_api.textfield()->text().empty());
EXPECT_TRUE(password_test_api.textfield()->read_only());
}
// Verifies that tabbing from the lock screen will eventually focus the shelf.
// Then, a shift+tab will bring focus back to the lock screen.
TEST_F(LockScreenSanityTest, TabGoesFromLockToShelfAndBackToLock) {
// Make lock screen shelf visible.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
// Create lock screen.
auto* lock = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
->shelf_widget()
->GetContentsView();
// Lock has focus.
EXPECT_TRUE(VerifyFocused(lock));
EXPECT_TRUE(VerifyNotFocused(shelf));
// Tab (eventually) goes to the shelf.
ASSERT_TRUE(TabThroughView(&GetEventGenerator(), lock, false /*reverse*/));
EXPECT_TRUE(VerifyNotFocused(lock));
EXPECT_TRUE(VerifyFocused(shelf));
// A single shift+tab brings focus back to the lock screen.
GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(VerifyFocused(lock));
EXPECT_TRUE(VerifyNotFocused(shelf));
}
// Verifies that shift-tabbing from the lock screen will eventually focus the
// status area. Then, a tab will bring focus back to the lock screen.
TEST_F(LockScreenSanityTest, ShiftTabGoesFromLockToStatusAreaAndBackToLock) {
// The status area will not focus out unless we are on the lock screen. See
// StatusAreaWidgetDelegate::ShouldFocusOut.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
auto* lock = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
views::View* status_area =
RootWindowController::ForWindow(lock->GetWidget()->GetNativeWindow())
->GetSystemTray()
->GetWidget()
->GetContentsView();
// Lock screen has focus.
EXPECT_TRUE(VerifyFocused(lock));
EXPECT_TRUE(VerifyNotFocused(status_area));
// Focus from user view to the status area.
GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(VerifyNotFocused(lock));
EXPECT_TRUE(VerifyFocused(status_area));
// A single tab brings focus back to the lock screen.
GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, 0);
EXPECT_TRUE(VerifyFocused(lock));
EXPECT_TRUE(VerifyNotFocused(status_area));
}
TEST_F(LockScreenSanityTest, TabWithLockScreenAppActive) {
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
auto* lock = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
->shelf_widget()
->GetContentsView();
views::View* status_area =
RootWindowController::ForWindow(lock->GetWidget()->GetNativeWindow())
->GetSystemTray()
->GetWidget()
->GetContentsView();
LoginScreenController* controller = Shell::Get()->login_screen_controller();
// Initialize lock screen action state.
data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kActive);
// Create and focus a lock screen app window.
auto* lock_screen_app = new views::View();
lock_screen_app->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
std::unique_ptr<views::Widget> app_widget =
CreateWidgetWithContent(lock_screen_app);
app_widget->Show();
// Lock screen app focus is requested using lock screen mojo client - set up
// the mock client.
LockScreenAppFocuser app_widget_focuser(app_widget.get());
std::unique_ptr<MockLoginScreenClient> client = BindMockLoginScreenClient();
EXPECT_CALL(*client, FocusLockScreenApps(_))
.WillRepeatedly(Invoke(&app_widget_focuser,
&LockScreenAppFocuser::FocusLockScreenApp));
// Initially, focus should be with the lock screen app - when the app loses
// focus (notified via mojo interface), shelf should get the focus next.
EXPECT_TRUE(VerifyFocused(lock_screen_app));
controller->HandleFocusLeavingLockScreenApps(false /*reverse*/);
EXPECT_TRUE(VerifyFocused(shelf));
// Reversing focus should bring focus back to the lock screen app.
GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_SHIFT_DOWN);
// Focus is passed to lock screen apps via mojo - flush the request.
controller->FlushForTesting();
EXPECT_TRUE(VerifyFocused(lock_screen_app));
EXPECT_TRUE(app_widget_focuser.reversed_tab_order());
// Have the app tab out in reverse tab order - in this case, the status area
// should get the focus.
controller->HandleFocusLeavingLockScreenApps(true /*reverse*/);
EXPECT_TRUE(VerifyFocused(status_area));
// Tabbing out of the status area (in default order) should focus the lock
// screen app again.
GetEventGenerator().PressKey(ui::KeyboardCode::VKEY_TAB, 0);
// Focus is passed to lock screen apps via mojo - flush the request.
controller->FlushForTesting();
EXPECT_TRUE(VerifyFocused(lock_screen_app));
EXPECT_FALSE(app_widget_focuser.reversed_tab_order());
// Tab out of the lock screen app once more - the shelf should get the focus
// again.
controller->HandleFocusLeavingLockScreenApps(false /*reverse*/);
EXPECT_TRUE(VerifyFocused(shelf));
}
TEST_F(LockScreenSanityTest, FocusLockScreenWhenLockScreenAppExit) {
// Set up lock screen.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
auto* lock = new LockContentsView(
mojom::TrayActionState::kNotAvailable, data_dispatcher(),
std::make_unique<FakeLoginDetachableBaseModel>(data_dispatcher()));
SetUserCount(1);
std::unique_ptr<views::Widget> widget = CreateWidgetWithContent(lock);
views::View* shelf = Shelf::ForWindow(lock->GetWidget()->GetNativeWindow())
->shelf_widget()
->GetContentsView();
// Setup and focus a lock screen app.
data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kActive);
auto* lock_screen_app = new views::View();
lock_screen_app->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
std::unique_ptr<views::Widget> app_widget =
CreateWidgetWithContent(lock_screen_app);
app_widget->Show();
EXPECT_TRUE(VerifyFocused(lock_screen_app));
// Tab out of the lock screen app - shelf should get the focus.
Shell::Get()->login_screen_controller()->HandleFocusLeavingLockScreenApps(
false /*reverse*/);
EXPECT_TRUE(VerifyFocused(shelf));
// Move the lock screen note taking to available state (which happens when the
// app session ends) - this should focus the lock screen.
data_dispatcher()->SetLockScreenNoteState(mojom::TrayActionState::kAvailable);
EXPECT_TRUE(VerifyFocused(lock));
// Tab through the lock screen - the focus should eventually get to the shelf.
ASSERT_TRUE(TabThroughView(&GetEventGenerator(), lock, false /*reverse*/));
EXPECT_TRUE(VerifyFocused(shelf));
}
} // namespace ash