blob: 5d86e0ce3836a3e639d249a4418c99d0ca4e4729 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/location_bar/location_bar.h"
#include "chrome/browser/ui/omnibox/omnibox_tab_helper.h"
#include "chrome/browser/ui/toasts/api/toast_id.h"
#include "chrome/browser/ui/toasts/toast_controller.h"
#include "chrome/browser/ui/toasts/toast_features.h"
#include "chrome/browser/ui/toasts/toast_view.h"
#include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/star_view.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/interactive_test.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/interaction/interactive_views_test.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kFirstTab);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSecondTab);
class OmniboxInputWaiter : public OmniboxTabHelper::Observer {
public:
explicit OmniboxInputWaiter(content::WebContents* web_contents) {
omnibox_helper_observer_.Observe(
OmniboxTabHelper::FromWebContents(web_contents));
run_loop_ = std::make_unique<base::RunLoop>(
base::RunLoop::Type::kNestableTasksAllowed);
}
~OmniboxInputWaiter() override = default;
void Wait() { run_loop_->Run(); }
void OnOmniboxInputStateChanged() override {}
void OnOmniboxInputInProgress(bool in_progress) override {
run_loop_->Quit();
}
void OnOmniboxFocusChanged(OmniboxFocusState state,
OmniboxFocusChangeReason reason) override {}
void OnOmniboxPopupVisibilityChanged(bool popup_is_open) override {}
private:
std::unique_ptr<base::RunLoop> run_loop_;
base::ScopedObservation<OmniboxTabHelper, OmniboxTabHelper::Observer>
omnibox_helper_observer_{this};
};
} // namespace
class ToastControllerInteractiveTest : public InteractiveBrowserTest {
public:
void SetUp() override {
feature_list_.InitWithFeatures(
{toast_features::kToastFramework, toast_features::kLinkCopiedToast,
toast_features::kImageCopiedToast, toast_features::kReadingListToast},
{});
InteractiveBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
InteractiveBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
GURL GetURL(std::string_view hostname = "example.com",
std::string_view path = "/title1.html") {
return embedded_test_server()->GetURL(hostname, path);
}
ToastController* GetToastController() {
return browser()->browser_window_features()->toast_controller();
}
auto ShowToast(ToastParams params) {
return Do(
[&]() { GetToastController()->MaybeShowToast(std::move(params)); });
}
auto FireToastCloseTimer() {
return Do([=, this]() {
GetToastController()->GetToastCloseTimerForTesting()->FireNow();
});
}
auto CheckShowingToastId(ToastId expected_id) {
return CheckResult(
[=, this]() {
ToastController* const toast_controller = GetToastController();
std::optional<ToastId> current_toast_id =
toast_controller->GetCurrentToastId();
return current_toast_id.value();
},
expected_id);
}
auto AdvanceKeyboardFocus(bool reverse) {
return Do([this, reverse]() {
ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
browser(), ui::VKEY_TAB, false, reverse, false, false));
});
}
void RemoveOmniboxFocus() {
ui_test_utils::ClickOnView(
BrowserView::GetBrowserViewForBrowser(browser())->contents_web_view());
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, ShowEphemeralToast) {
RunTestSequence(
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
Check([=, this]() { return GetToastController()->IsShowingToast(); }));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
ShowSameEphemeralToastTwice) {
RunTestSequence(
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
Check([=, this]() { return GetToastController()->IsShowingToast(); }),
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
Check([=, this]() { return GetToastController()->IsShowingToast(); }));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, PreemptEphemeralToast) {
RunTestSequence(
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
Check([=, this]() { return GetToastController()->IsShowingToast(); }),
ShowToast(ToastParams(ToastId::kImageCopied)));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, ShowPersistentToast) {
RunTestSequence(ShowToast(ToastParams(ToastId::kLensOverlay)),
WaitForShow(toasts::ToastView::kToastViewId), Check([=, this]() {
return GetToastController()->IsShowingToast();
}));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, PersistentToastHides) {
RunTestSequence(
ShowToast(ToastParams(ToastId::kLensOverlay)),
WaitForShow(toasts::ToastView::kToastViewId), Do([=, this]() {
GetToastController()->ClosePersistentToast(ToastId::kLensOverlay);
}),
WaitForHide(toasts::ToastView::kToastViewId));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, PreemptPersistentToast) {
RunTestSequence(
ShowToast(ToastParams(ToastId::kLensOverlay)),
WaitForShow(toasts::ToastView::kToastViewId),
Check([=, this]() { return GetToastController()->IsShowingToast(); }),
CheckShowingToastId(ToastId::kLensOverlay),
ShowToast(ToastParams(ToastId::kLinkCopied)),
// Ephemeral Toast should force the persistent toast to close
WaitForHide(toasts::ToastView::kToastViewId),
// After the persistent toast closes, the ephemeral toast should show
WaitForShow(toasts::ToastView::kToastViewId),
CheckShowingToastId(ToastId::kLinkCopied),
// Simulate the ephemeral toast timing out and auto dismiss
FireToastCloseTimer(), WaitForHide(toasts::ToastView::kToastViewId),
// Persistent toast should reshow
WaitForShow(toasts::ToastView::kToastViewId),
CheckShowingToastId(ToastId::kLensOverlay));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, FocusNextPane) {
ui::Accelerator next_pane;
ASSERT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())->GetAccelerator(
IDC_FOCUS_NEXT_PANE, &next_pane));
views::Widget* toast_widget = nullptr;
RunTestSequence(
ObserveState(views::test::kCurrentWidgetFocus),
ShowToast(ToastParams(ToastId::kAddedToReadingList)),
WaitForShow(toasts::ToastView::kToastViewId),
WithView(
toasts::ToastView::kToastViewId,
[&](toasts::ToastView* toast) { toast_widget = toast->GetWidget(); }),
CheckView(toasts::ToastView::kToastViewId,
[](toasts::ToastView* toast) {
return !toast->GetFocusManager()->GetFocusedView();
}),
SendAccelerator(kBrowserViewElementId, next_pane),
WaitForState(views::test::kCurrentWidgetFocus,
[&]() { return toast_widget->GetNativeView(); }),
CheckView(toasts::ToastView::kToastViewId, [](toasts::ToastView* toast) {
return toast->GetFocusManager()->GetFocusedView() ==
toast->action_button_for_testing();
}));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, ReverseFocusTraversal) {
ui::Accelerator next_pane;
ASSERT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())->GetAccelerator(
IDC_FOCUS_NEXT_PANE, &next_pane));
RunTestSequence(
ObserveState(views::test::kCurrentWidgetFocus),
ShowToast(ToastParams(ToastId::kAddedToReadingList)),
WaitForShow(toasts::ToastView::kToastViewId),
ActivateSurface(toasts::ToastView::kToastViewId),
SendAccelerator(kBrowserViewElementId, next_pane),
CheckView(toasts::ToastView::kToastViewId,
[](toasts::ToastView* toast) {
return toast->GetFocusManager()->GetFocusedView() ==
toast->action_button_for_testing();
}),
AdvanceKeyboardFocus(true),
#if BUILDFLAG(IS_MAC)
// Mac focus traversal order is slightly different from other platforms
CheckView(kToolbarAppMenuButtonElementId,
[](AppMenuButton* button) { return button->HasFocus(); })
#else
CheckView(kBookmarkStarViewElementId,
[](StarView* star_view) { return star_view->HasFocus(); })
#endif
);
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest, ForwardFocusTraversal) {
ui::Accelerator next_pane;
ASSERT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())->GetAccelerator(
IDC_FOCUS_NEXT_PANE, &next_pane));
RunTestSequence(
ObserveState(views::test::kCurrentWidgetFocus),
ShowToast(ToastParams(ToastId::kAddedToReadingList)),
WaitForShow(toasts::ToastView::kToastViewId),
ActivateSurface(toasts::ToastView::kToastViewId),
SendAccelerator(kBrowserViewElementId, next_pane),
// Advancing focus should move into the toast close button
AdvanceKeyboardFocus(false),
CheckView(toasts::ToastView::kToastViewId,
[](toasts::ToastView* toast) {
return toast->close_button_for_testing()->HasFocus();
}),
// Advancing focus again should move out of the toast and into the WebView
AdvanceKeyboardFocus(false),
CheckView(toasts::ToastView::kToastViewId,
[](toasts::ToastView* toast) {
return !toast->close_button_for_testing()->HasFocus();
}),
CheckView(kBrowserViewElementId, [](BrowserView* browser_view) {
return browser_view->GetContentsWebView()->HasFocus();
}));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
HideTabScopedToastOnTabChange) {
RunTestSequence(InstrumentTab(kFirstTab),
AddInstrumentedTab(kSecondTab, GetURL()),
SelectTab(kTabStripElementId, 0), WaitForShow(kFirstTab),
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
SelectTab(kTabStripElementId, 1),
WaitForHide(toasts::ToastView::kToastViewId));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
GlobalScopedToastStaysOnTabChange) {
RunTestSequence(InstrumentTab(kFirstTab),
AddInstrumentedTab(kSecondTab, GetURL()),
SelectTab(kTabStripElementId, 0), WaitForShow(kFirstTab),
ShowToast(ToastParams(ToastId::kNonMilestoneUpdate)),
WaitForShow(toasts::ToastView::kToastViewId),
SelectTab(kTabStripElementId, 1),
EnsurePresent(toasts::ToastView::kToastViewId));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
HideTabScopedToastOnNavigation) {
RunTestSequence(InstrumentTab(kFirstTab),
ShowToast(ToastParams(ToastId::kLinkCopied)),
WaitForShow(toasts::ToastView::kToastViewId),
NavigateWebContents(kFirstTab, GetURL()),
WaitForHide(toasts::ToastView::kToastViewId));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
GlobalScopedToastStaysOnNavigation) {
RunTestSequence(InstrumentTab(kFirstTab),
ShowToast(ToastParams(ToastId::kNonMilestoneUpdate)),
WaitForShow(toasts::ToastView::kToastViewId),
NavigateWebContents(kFirstTab, GetURL()),
EnsurePresent(toasts::ToastView::kToastViewId));
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
ToastReactToOmniboxFocus) {
LocationBar* const location_bar = browser()->window()->GetLocationBar();
ASSERT_TRUE(location_bar);
OmniboxView* const omnibox_view = location_bar->GetOmniboxView();
ASSERT_TRUE(omnibox_view);
browser()->window()->SetFocusToLocationBar(true);
ASSERT_FALSE(omnibox_view->model()->PopupIsOpen());
// Even though the omnibox is focused, the toast should still show because
// the omnibox doesn't have a popup and the user isn't interacting with the
// omnibox.
ToastController* const toast_controller = GetToastController();
EXPECT_TRUE(
toast_controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Omnibox should still show even when focus is removed from the omnibox.
RemoveOmniboxFocus();
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Focus the omnibox again should cause the toast to no longer be visible
// because we are focusing after the toast is already shown.
browser()->window()->SetFocusToLocationBar(true);
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_FALSE(toast_controller->GetToastWidgetForTesting()->IsVisible());
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
HidesWhenOmniboxPopupShows) {
// Even though the omnibox is focused, the toast should still show because
// the omnibox doesn't have a popup and the user isn't interacting with the
// omnibox.
ToastController* const toast_controller = GetToastController();
EXPECT_TRUE(
toast_controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Trigger the omnibox popup to show.
LocationBar* const location_bar = browser()->window()->GetLocationBar();
ASSERT_TRUE(location_bar);
OmniboxView* const omnibox_view = location_bar->GetOmniboxView();
ASSERT_TRUE(omnibox_view);
ASSERT_FALSE(omnibox_view->model()->PopupIsOpen());
omnibox_view->OnBeforePossibleChange();
omnibox_view->SetUserText(u"hello world");
omnibox_view->OnAfterPossibleChange(true);
ASSERT_TRUE(omnibox_view->model()->PopupIsOpen());
// The toast widget should no longer be visible because there is a popup.
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_FALSE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Toast widget is visible again after the omnibox is no longer focused.
RemoveOmniboxFocus();
ASSERT_FALSE(omnibox_view->model()->PopupIsOpen());
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
}
IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
HidesWhenTypingInOmnibox) {
browser()->window()->SetFocusToLocationBar(true);
// Even though the omnibox is focused, the toast should still show because
// the omnibox doesn't have a popup and the user isn't interacting with the
// omnibox.
ToastController* const toast_controller = GetToastController();
EXPECT_TRUE(
toast_controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Start typing in the omnibox.
auto omnibox_input_waiter = std::make_unique<OmniboxInputWaiter>(
browser()->tab_strip_model()->GetActiveWebContents());
ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_A, false,
false, false, false));
omnibox_input_waiter->Wait();
// The toast widget should no longer be visible because we are typing.
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_FALSE(toast_controller->GetToastWidgetForTesting()->IsVisible());
// Toast widget is visible again after the omnibox is no longer focused.
RemoveOmniboxFocus();
EXPECT_TRUE(toast_controller->IsShowingToast());
EXPECT_TRUE(toast_controller->GetToastWidgetForTesting()->IsVisible());
}