blob: 86033d18e824608b4488ef4a130f2954b914c737 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/check.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service_factory.h"
#include "chrome/browser/signin/signin_promo.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/views/profiles/profile_picker_interactive_uitest_base.h"
#include "chrome/browser/ui/views/profiles/profile_picker_test_base.h"
#include "chrome/browser/ui/views/profiles/profile_picker_view.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/regional_capabilities/regional_capabilities_switches.h"
#include "components/search_engines/search_engines_switches.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
using testing::Eq;
using testing::IsFalse;
using testing::IsTrue;
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kProfilePickerViewId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kPickerWebContentsId);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonEnabled);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonDisabled);
const WebContentsInteractionTestUtil::DeepQuery kSignInButton = {
"profile-picker-app", "profile-type-choice", "#signInButton"};
const WebContentsInteractionTestUtil::DeepQuery kContinueWithoutAccountButton =
{"profile-picker-app", "profile-type-choice", "#notNowButton"};
const WebContentsInteractionTestUtil::DeepQuery kAddProfileButton = {
"profile-picker-app", "profile-picker-main-view", "#addProfile"};
// Waits until the widget bounds change.
class WidgetBoundsChangeWaiter : public views::WidgetObserver {
public:
explicit WidgetBoundsChangeWaiter(views::Widget* widget) {
DCHECK(widget);
observation_.Observe(widget);
}
// Waits until the widget bounds change.
void Wait() { run_loop_.Run(); }
private:
// WidgetObserver:
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) override {
run_loop_.Quit();
}
base::RunLoop run_loop_;
base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
this};
};
} // namespace
struct NavState {
int entry_count;
int last_committed_entry_index;
bool has_pending_entry = false;
bool operator==(const NavState& other) const {
return entry_count == other.entry_count &&
last_committed_entry_index == other.last_committed_entry_index &&
has_pending_entry == other.has_pending_entry;
}
friend std::ostream& operator<<(std::ostream& stream, const NavState& state) {
return stream << "{entry_count=" << state.entry_count
<< ", last_committed_entry_index="
<< state.last_committed_entry_index
<< ", has_pending_entry=" << state.has_pending_entry << "}";
}
};
class ProfilePickerInteractiveUiTest
: public InteractiveBrowserTest,
public WithProfilePickerInteractiveUiTestHelpers {
public:
ProfilePickerInteractiveUiTest() {
scoped_chrome_build_override_ = std::make_unique<base::AutoReset<bool>>(
SearchEngineChoiceDialogServiceFactory::
ScopedChromeBuildOverrideForTesting(
/*force_chrome_build=*/true));
}
~ProfilePickerInteractiveUiTest() override = default;
void ShowAndFocusPicker(ProfilePicker::EntryPoint entry_point,
const GURL& expected_url = GURL()) {
ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(entry_point));
WaitForPickerWidgetCreated();
view()->SetProperty(views::kElementIdentifierKey, kProfilePickerViewId);
if (!expected_url.is_empty()) {
WaitForLoadStop(expected_url);
}
EXPECT_TRUE(
ui_test_utils::ShowAndFocusNativeWindow(widget()->GetNativeWindow()));
}
auto GetNavState() {
return [this]() {
return NavState{
.entry_count = web_contents()->GetController().GetEntryCount(),
.last_committed_entry_index =
web_contents()->GetController().GetLastCommittedEntryIndex(),
.has_pending_entry =
web_contents()->GetController().GetPendingEntry() != nullptr,
};
};
}
auto HasPendingNav() {
return [this]() {
return web_contents()->GetController().GetPendingEntry() != nullptr;
};
}
StateChange Exists(const DeepQuery& where) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementExistsEvent);
StateChange state_change;
state_change.type = StateChange::Type::kExists;
state_change.where = where;
state_change.event = kElementExistsEvent;
return state_change;
}
// Used to wait for screen changes inside of the `profile-picker-app`, in
// combination with `WaitForStateChange()`.
//
// In the profile picker app we modify the displayed URL but don't do a full
// navigation, so `WaitForWebContentsNavigation()` does not detect these
// changes.
StateChange UrlEntryMatches(const GURL& url) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kUrlEntryMatchesEvent);
StateChange state_change;
state_change.test_function = base::StringPrintf(
"(_) => navigation && navigation.currentEntry && "
"navigation.currentEntry.url === '%s'",
url.spec().c_str());
state_change.type = StateChange::Type::kConditionTrue;
state_change.event = kUrlEntryMatchesEvent;
return state_change;
}
void SetUpOnMainThread() override {
InteractiveBrowserTest::SetUpOnMainThread();
SearchEngineChoiceDialogService::SetDialogDisabledForTests(
/*dialog_disabled=*/false);
}
void SetUpCommandLine(base::CommandLine* command_line) override {
InteractiveBrowserTest::SetUpCommandLine(command_line);
// Change the country to belgium because the search engine choice screen
// is only displayed for EEA countries.
command_line->AppendSwitchASCII(switches::kSearchEngineChoiceCountry, "BE");
command_line->AppendSwitch(
switches::kIgnoreNoFirstRunForSearchEngineChoiceScreen);
}
const base::HistogramTester& HistogramTester() const {
return histogram_tester_;
}
const base::UserActionTester& UserActionTester() const {
return user_action_tester_;
}
auto WaitForButtonEnabled(const ui::ElementIdentifier web_contents_id,
const DeepQuery& button_query) {
StateChange button_enabled;
button_enabled.event = kButtonEnabled;
button_enabled.where = button_query;
button_enabled.type = StateChange::Type::kExistsAndConditionTrue;
button_enabled.test_function = "(btn) => !btn.disabled";
return WaitForStateChange(web_contents_id, button_enabled);
}
auto WaitForButtonDisabled(const ui::ElementIdentifier web_contents_id,
const DeepQuery& button_query) {
StateChange button_disabled;
button_disabled.event = kButtonDisabled;
button_disabled.where = button_query;
button_disabled.type = StateChange::Type::kExistsAndConditionTrue;
button_disabled.test_function = "(btn) => btn.disabled";
return WaitForStateChange(web_contents_id, button_disabled);
}
auto PressJsButton(const ui::ElementIdentifier web_contents_id,
const DeepQuery& button_query) {
// This can close/navigate the current page, so don't wait for success.
return ExecuteJsAt(web_contents_id, button_query, "(btn) => btn.click()",
ExecuteJsMode::kFireAndForget);
}
auto CompleteSearchEngineChoiceStep() {
const WebContentsInteractionTestUtil::DeepQuery
kSearchEngineChoiceActionButton{"search-engine-choice-app",
"#actionButton"};
const DeepQuery first_search_engine = {"search-engine-choice-app",
"cr-radio-button"};
const DeepQuery searchEngineChoiceList{"search-engine-choice-app",
"#choiceList"};
return Steps(
WaitForWebContentsNavigation(
kPickerWebContentsId, GURL(chrome::kChromeUISearchEngineChoiceURL)),
Do([&] {
HistogramTester().ExpectBucketCount(
search_engines::kSearchEngineChoiceScreenEventsHistogram,
search_engines::SearchEngineChoiceScreenEvents::
kProfileCreationChoiceScreenWasDisplayed,
1);
EXPECT_EQ(UserActionTester().GetActionCount(
"SearchEngineChoiceScreenShown"),
1);
}),
// Click on "More" to scroll to the bottom of the search engine list.
PressJsButton(kPickerWebContentsId, kSearchEngineChoiceActionButton),
// The button should become disabled because we didn't make a choice.
WaitForButtonDisabled(kPickerWebContentsId,
kSearchEngineChoiceActionButton),
PressJsButton(kPickerWebContentsId, first_search_engine),
WaitForButtonEnabled(kPickerWebContentsId,
kSearchEngineChoiceActionButton),
PressJsButton(kPickerWebContentsId, kSearchEngineChoiceActionButton));
}
private:
std::unique_ptr<base::AutoReset<bool>> scoped_chrome_build_override_;
base::UserActionTester user_action_tester_;
base::HistogramTester histogram_tester_;
};
// Checks that the main picker view can be closed with keyboard shortcut.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, CloseWithKeyboard) {
// Open a new picker.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
EXPECT_TRUE(ProfilePicker::IsOpen());
SendCloseWindowKeyboardCommand();
WaitForPickerClosed();
// Closing the picker does not exit Chrome.
EXPECT_FALSE(browser_shutdown::IsTryingToQuit());
}
#if BUILDFLAG(IS_MAC)
// Checks that Chrome be closed with keyboard shortcut. Only MacOS has a
// keyboard shortcut to exit Chrome.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, ExitWithKeyboard) {
// Open a new picker.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
EXPECT_TRUE(ProfilePicker::IsOpen());
SendQuitAppKeyboardCommand();
WaitForPickerClosed();
EXPECT_TRUE(browser_shutdown::IsTryingToQuit());
}
#endif
// Checks that the main picker view can switch to full screen.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, FullscreenWithKeyboard) {
// Open a new picker.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
EXPECT_TRUE(ProfilePicker::IsOpen());
EXPECT_FALSE(widget()->IsFullscreen());
WidgetBoundsChangeWaiter bounds_waiter(widget());
SendToggleFullscreenKeyboardCommand();
// Fullscreen causes the bounds of the widget to change.
bounds_waiter.Wait();
EXPECT_TRUE(widget()->IsFullscreen());
}
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
CloseDiceSigninWithKeyboard) {
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuAddNewProfile);
RunTestSequenceInContext(
views::ElementTrackerViews::GetContextForView(view()),
// Wait for the profile picker to show the profile type choice screen.
WaitForShow(kProfilePickerViewId),
InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
WaitForWebContentsReady(kPickerWebContentsId,
GURL("chrome://profile-picker/new-profile")),
// Click "sign in".
// Note: the button should be disabled after this, but there is no good
// way to verify it in this sequence. It is verified by unit tests in
// chrome/test/data/webui/signin/profile_picker_app_test.ts
EnsurePresent(kPickerWebContentsId, kSignInButton),
MoveMouseTo(kPickerWebContentsId, kSignInButton), ClickMouse(),
// Wait for switch to the Gaia sign-in page to complete.
// Note: kPickerWebContentsId now points to the new profile's WebContents.
WaitForWebContentsNavigation(kPickerWebContentsId,
GetSigninChromeSyncDiceUrl()),
// Send "Close window" keyboard shortcut and wait for view to close.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_CLOSE_WINDOW)),
WaitForHide(kProfilePickerViewId, /*transition_only_on_event=*/true),
// Note: The widget/view is destroyed asynchronously, we need to flush the
// message loops to be able to reliably check the global state.
CheckResult(&ProfilePicker::IsOpen, testing::IsFalse()));
}
// Checks that both the signin web view and the main picker view are able to
// process a back keyboard event.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
NavigateBackFromDiceSigninWithKeyboard) {
// Simulate walking through the flow starting at the picker so that navigating
// back to the picker makes sense. Check that the navigation list is populated
// correctly.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
RunTestSequenceInContext(
views::ElementTrackerViews::GetContextForView(view()),
WaitForShow(kProfilePickerViewId),
InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
WaitForWebContentsReady(kPickerWebContentsId,
GURL("chrome://profile-picker")),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
.last_committed_entry_index = 0})),
// Advance to the profile type choice screen.
EnsurePresent(kPickerWebContentsId, kAddProfileButton),
MoveMouseTo(kPickerWebContentsId, kAddProfileButton), ClickMouse(),
WaitForStateChange(
kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 1})),
// Advance to the sign-in page.
WaitForStateChange(kPickerWebContentsId, Exists(kSignInButton)),
MoveMouseTo(kPickerWebContentsId, kSignInButton), ClickMouse(),
WaitForWebContentsNavigation(kPickerWebContentsId,
GetSigninChromeSyncDiceUrl()),
// Navigate back with the keyboard, switch back to the picker.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WaitForStateChange(
kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 1})),
// Navigate again back with the keyboard.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsTrue())),
WaitForStateChange(kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 0})),
// Navigating back once again does nothing.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsFalse())));
}
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
NavigateBackFromProfileTypeChoiceWithKeyboard) {
// Simulate walking through the flow starting at the picker so that navigating
// back to the picker makes sense. Check that the navigation list is populated
// correctly.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
RunTestSequenceInContext(
views::ElementTrackerViews::GetContextForView(view()),
WaitForShow(kProfilePickerViewId),
InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
WaitForWebContentsReady(kPickerWebContentsId,
GURL("chrome://profile-picker")),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
.last_committed_entry_index = 0})),
// Advance to the profile type choice screen.
EnsurePresent(kPickerWebContentsId, kAddProfileButton),
MoveMouseTo(kPickerWebContentsId, kAddProfileButton), ClickMouse(),
WaitForStateChange(
kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 1})),
// Navigate back with the keyboard.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsTrue())),
WaitForStateChange(kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 0})),
// Navigating back once again does nothing.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsFalse())));
}
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
NavigateBackFromNewProfileWithKeyboard) {
// Check that when deep-linking into the flow via the "Add profile" menu entry
// populates the navigation list is populated correctly such that back
// navigations make sense.
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuAddNewProfile,
GURL("chrome://profile-picker/new-profile"));
RunTestSequenceInContext(
views::ElementTrackerViews::GetContextForView(view()),
WaitForShow(kProfilePickerViewId),
InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
WaitForWebContentsReady(kPickerWebContentsId,
GURL("chrome://profile-picker/new-profile")),
// Even though we start straight on the "new-profile" page, the picker
// main view should be loaded under it in the nav stack.
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 1})),
// Focus the window to ensure the keyboard shortcut reaches it.
FocusWebContents(kPickerWebContentsId),
// Navigate back with the keyboard.
SendAccelerator(kPickerWebContentsId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsTrue(),
/*check_description=*/"HasPendingNav")),
WaitForStateChange(kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 0})),
// Navigating back once again does nothing.
SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
WithoutDelay(CheckResult(HasPendingNav(), IsFalse())));
}
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, ContinueWithoutAccount) {
ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
GURL("chrome://profile-picker"));
RunTestSequenceInContext(
views::ElementTrackerViews::GetContextForView(view()),
WaitForShow(kProfilePickerViewId),
InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
WaitForWebContentsReady(kPickerWebContentsId,
GURL("chrome://profile-picker")),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
.last_committed_entry_index = 0})),
// Advance to the profile type choice screen.
EnsurePresent(kPickerWebContentsId, kAddProfileButton),
PressJsButton(kPickerWebContentsId, kAddProfileButton)
.SetMustRemainVisible(false),
WaitForStateChange(
kPickerWebContentsId,
UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
.last_committed_entry_index = 1})),
// Advance to the post signed out flow
WaitForStateChange(kPickerWebContentsId,
Exists(kContinueWithoutAccountButton)),
PressJsButton(kPickerWebContentsId, kContinueWithoutAccountButton)
.SetMustRemainVisible(false),
CompleteSearchEngineChoiceStep());
WaitForPickerClosed();
HistogramTester().ExpectUniqueSample(
"Profile.AddNewUser", ProfileMetrics::ADD_NEW_PROFILE_PICKER_LOCAL, 1);
HistogramTester().ExpectBucketCount(
search_engines::kSearchEngineChoiceScreenEventsHistogram,
search_engines::SearchEngineChoiceScreenEvents::
kProfileCreationDefaultWasSet,
1);
}