blob: ff480a00f2d276b45ce475394bae4f7ecf44ee4b [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/link_capturing/link_capturing_feature_test_support.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/user_education/browser_feature_promo_controller.h"
#include "chrome/browser/ui/views/web_apps/web_app_link_capturing_test_utils.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/test/web_app_navigation_browsertest.h"
#include "chrome/browser/web_applications/test/web_app_icon_test_utils.h"
#include "chrome/browser/web_applications/web_app_pref_guardrails.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/user_education/interactive_feature_promo_test.h"
#include "components/user_education/views/help_bubble_factory_views.h"
#include "components/user_education/views/help_bubble_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/widget/any_widget_observer.h"
#if BUILDFLAG(IS_MAC)
#include "chrome/browser/apps/link_capturing/mac_intent_picker_helpers.h"
#endif // BUILDFLAG(IS_MAC)
// This test only runs on Windows, Mac and Linux platforms.
// Because some tests here rely on browser activation, an
// interactive_ui_test is preferred over a browser test.
class DefaultLinkCapturingInteractiveUiTest
: public web_app::WebAppNavigationBrowserTest {
public:
DefaultLinkCapturingInteractiveUiTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
apps::test::GetFeaturesToEnableLinkCapturingUX(), {});
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
size_t GetItemContainerSize(IntentPickerBubbleView* bubble) {
return bubble->GetViewByID(IntentPickerBubbleView::ViewId::kItemContainer)
->children()
.size();
}
GURL GetOuterUrl() {
return embedded_test_server()->GetURL("/web_apps/nesting/index.html");
}
GURL GetInnerNestedUrl() {
return embedded_test_server()->GetURL(
"/web_apps/nesting/nested/index.html");
}
GURL GetNestedPageUrl() {
return embedded_test_server()->GetURL(
"/web_apps/nesting/nested/page1.html");
}
// Returns [outer app_id, inner app_id]
std::tuple<webapps::AppId, webapps::AppId> InstallOuterAppAndInnerApp() {
// The inner app must be installed first so that it is installable.
webapps::AppId inner_app_id =
web_app::InstallWebAppFromPageAndCloseAppBrowser(browser(),
GetInnerNestedUrl());
webapps::AppId outer_app_id =
web_app::InstallWebAppFromPageAndCloseAppBrowser(browser(),
GetOuterUrl());
return {outer_app_id, inner_app_id};
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(DefaultLinkCapturingInteractiveUiTest,
IntentPickerBubbleAcceptCorrectActions) {
const auto [outer_app_id, inner_app_id] = InstallOuterAppAndInnerApp();
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetNestedPageUrl()));
EXPECT_TRUE(web_app::ClickIntentPickerAndWaitForBubble(browser()));
base::UserActionTester user_action_tester;
// The app list is currently not deterministically ordered, so find the
// correct item and select that.
const auto& app_infos =
web_app::intent_picker_bubble()->app_info_for_testing(); // IN-TEST
auto it = base::ranges::find(app_infos, outer_app_id,
&apps::IntentPickerAppInfo::launch_name);
ASSERT_NE(it, app_infos.end());
size_t index = it - app_infos.begin();
ui_test_utils::BrowserChangeObserver browser_added_waiter(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
views::test::ButtonTestApi test_api(
web_app::GetIntentPickerButtonAtIndex(index));
test_api.NotifyClick(ui::MouseEvent(
ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(), base::TimeTicks(),
ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON));
web_app::intent_picker_bubble()->AcceptDialog();
EXPECT_EQ(
1, user_action_tester.GetActionCount("IntentPickerViewAcceptLaunchApp"));
Browser* app_browser = browser_added_waiter.Wait();
ASSERT_TRUE(app_browser);
EXPECT_TRUE(web_app::AppBrowserController::IsWebApp(app_browser));
EXPECT_TRUE(
web_app::AppBrowserController::IsForWebApp(app_browser, outer_app_id));
}
IN_PROC_BROWSER_TEST_F(DefaultLinkCapturingInteractiveUiTest,
IntentPickerBubbleCancel) {
const auto [outer_app_id, inner_app_id] = InstallOuterAppAndInnerApp();
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetNestedPageUrl()));
base::UserActionTester user_action_tester;
EXPECT_TRUE(web_app::ClickIntentPickerAndWaitForBubble(browser()));
web_app::intent_picker_bubble()->CancelDialog();
EXPECT_EQ(1, user_action_tester.GetActionCount(
"IntentPickerViewClosedStayInChrome"));
// Verify no new browsers have opened.
EXPECT_EQ(1u, chrome::GetTotalBrowserCount());
}
IN_PROC_BROWSER_TEST_F(DefaultLinkCapturingInteractiveUiTest,
IntentPickerBubbleIgnored) {
const auto [outer_app_id, inner_app_id] = InstallOuterAppAndInnerApp();
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetNestedPageUrl()));
base::UserActionTester user_action_tester;
EXPECT_TRUE(web_app::ClickIntentPickerAndWaitForBubble(browser()));
// Opening a new tab should ignore the current intent picker view.
chrome::NewTab(browser());
EXPECT_EQ(1, user_action_tester.GetActionCount("IntentPickerViewIgnored"));
// Verify no new browsers have opened.
EXPECT_EQ(1u, chrome::GetTotalBrowserCount());
}
#if BUILDFLAG(IS_MAC)
using IntentPickerInteractiveUiTest = DefaultLinkCapturingInteractiveUiTest;
// Test that if there is a Universal Link for a site, it shows the picker
// with the app icon.
IN_PROC_BROWSER_TEST_F(IntentPickerInteractiveUiTest,
ShowUniversalLinkAppInIntentChip) {
const GURL url1(embedded_test_server()->GetURL("/title1.html"));
const GURL url2(embedded_test_server()->GetURL("/title2.html"));
const char* kFinderAppPath = "/System/Library/CoreServices/Finder.app";
// Start with a page with no corresponding native app.
apps::OverrideMacAppForUrlForTesting(true, "");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url1));
// Verify that no icon was shown.
EXPECT_TRUE(web_app::AwaitIntentPickerTabHelperIconUpdateComplete(
browser()->tab_strip_model()->GetActiveWebContents()));
ASSERT_FALSE(web_app::GetIntentPickerIcon(browser())->GetVisible());
// Load a different page while simulating it having a native app.
apps::OverrideMacAppForUrlForTesting(true, kFinderAppPath);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url2));
// Verify app icon shows up in the intent picker.
EXPECT_TRUE(web_app::AwaitIntentPickerTabHelperIconUpdateComplete(
browser()->tab_strip_model()->GetActiveWebContents()));
IntentChipButton* intent_picker_icon =
web_app::GetIntentPickerIcon(browser());
ASSERT_NE(intent_picker_icon, nullptr);
ui::ImageModel app_icon = intent_picker_icon->GetAppIconForTesting();
SkColor final_color = app_icon.GetImage().AsBitmap().getColor(8, 8);
EXPECT_TRUE(
web_app::AreColorsEqual(SK_ColorRED, final_color, /*threshold=*/50));
}
#endif // BUILDFLAG(IS_MAC)
// Test to verify that launching an app from the intent picker chip shows the
// IPH bubble if the feature flag is enabled.
class WebAppLinkCapturingIPHPromoTest
: public InteractiveFeaturePromoTestT<
DefaultLinkCapturingInteractiveUiTest>,
public testing::WithParamInterface<bool> {
public:
WebAppLinkCapturingIPHPromoTest()
: InteractiveFeaturePromoTestT(UseDefaultTrackerAllowingPromos(
{feature_engagement::kIPHDesktopPWAsLinkCapturingLaunch})) {}
protected:
GURL GetAppUrl() {
return embedded_test_server()->GetURL("/web_apps/nesting/index.html");
}
webapps::AppId InstallApp() {
GURL start_url = GetAppUrl();
webapps::AppId app_id =
web_app::InstallWebAppFromPageAndCloseAppBrowser(browser(), start_url);
return app_id;
}
bool LinkCapturingEnabled() { return GetParam(); }
void Navigate(Browser* browser,
const GURL& url,
LinkTarget link_target = LinkTarget::SELF) {
ClickLinkAndWait(browser->tab_strip_model()->GetActiveWebContents(), url,
link_target, "");
}
BrowserFeaturePromoController* GetFeaturePromoController(Browser* browser) {
auto* promo_controller = static_cast<BrowserFeaturePromoController*>(
browser->window()->GetFeaturePromoController());
return promo_controller;
}
user_education::HelpBubbleView* GetCurrentPromoBubble(Browser* browser) {
auto* const promo_controller = GetFeaturePromoController(browser);
return promo_controller->promo_bubble_for_testing()
->AsA<user_education::HelpBubbleViews>()
->bubble_view();
}
void SetUpSiteForLinkCapturingIphBubble(const webapps::AppId& app_id) {
EXPECT_EQ(
apps::test::EnableLinkCapturingByUser(browser()->profile(), app_id),
base::ok());
}
void AcceptCustomActionIPH(Browser* app_browser) {
auto* custom_action_button =
GetCurrentPromoBubble(app_browser)
->GetNonDefaultButtonForTesting(/*index=*/0);
views::test::InteractionTestUtilSimulatorViews::PressButton(
custom_action_button, ui::test::InteractionTestUtil::InputType::kMouse);
}
void DismissIPH(Browser* app_browser) {
auto* custom_action_button =
GetCurrentPromoBubble(app_browser)->GetDefaultButtonForTesting();
views::test::InteractionTestUtilSimulatorViews::PressButton(
custom_action_button, ui::test::InteractionTestUtil::InputType::kMouse);
}
Browser* TriggerAppLaunchIphAndGetBrowser(const webapps::AppId& app_id) {
ui_test_utils::BrowserChangeObserver browser_added_waiter(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
Navigate(browser(), GetAppUrl());
Browser* app_browser = browser_added_waiter.Wait();
EXPECT_TRUE(
web_app::AppBrowserController::IsForWebApp(app_browser, app_id));
EXPECT_NE(browser(), app_browser);
EXPECT_TRUE(web_app::WaitForIPHToShowIfAny(app_browser));
EXPECT_TRUE(app_browser->window()->IsFeaturePromoActive(
feature_engagement::kIPHDesktopPWAsLinkCapturingLaunch));
return app_browser;
}
};
IN_PROC_BROWSER_TEST_P(WebAppLinkCapturingIPHPromoTest,
AppLaunchFromIntentChipShowsIPH) {
const webapps::AppId app_id = InstallApp();
if (LinkCapturingEnabled()) {
EXPECT_EQ(
apps::test::EnableLinkCapturingByUser(browser()->profile(), app_id),
base::ok());
} else {
EXPECT_EQ(
apps::test::DisableLinkCapturingByUser(browser()->profile(), app_id),
base::ok());
}
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetAppUrl()));
ui_test_utils::BrowserChangeObserver browser_added_waiter(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
ASSERT_TRUE(web_app::ClickIntentPickerChip(browser()));
Browser* app_browser = browser_added_waiter.Wait();
ASSERT_TRUE(app_browser);
EXPECT_TRUE(web_app::AppBrowserController::IsForWebApp(app_browser, app_id));
if (LinkCapturingEnabled()) {
EXPECT_TRUE(web_app::WaitForIPHToShowIfAny(app_browser));
}
EXPECT_EQ(app_browser->window()->IsFeaturePromoActive(
feature_engagement::kIPHDesktopPWAsLinkCapturingLaunch),
LinkCapturingEnabled());
}
IN_PROC_BROWSER_TEST_P(WebAppLinkCapturingIPHPromoTest,
IPHShownOnLinkClick) {
const webapps::AppId app_id = InstallApp();
SetUpSiteForLinkCapturingIphBubble(app_id);
ui_test_utils::BrowserChangeObserver browser_added_waiter(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
Navigate(browser(), GetAppUrl());
Browser* app_browser = browser_added_waiter.Wait();
EXPECT_TRUE(web_app::AppBrowserController::IsForWebApp(app_browser, app_id));
EXPECT_NE(browser(), app_browser);
EXPECT_TRUE(web_app::WaitForIPHToShowIfAny(app_browser));
EXPECT_TRUE(app_browser->window()->IsFeaturePromoActive(
feature_engagement::kIPHDesktopPWAsLinkCapturingLaunch));
}
IN_PROC_BROWSER_TEST_P(WebAppLinkCapturingIPHPromoTest,
ClosingAppWindowMeasuresDismiss) {
const webapps::AppId app_id = InstallApp();
base::UserActionTester user_action_tester;
SetUpSiteForLinkCapturingIphBubble(app_id);
Browser* app_browser = TriggerAppLaunchIphAndGetBrowser(app_id);
EXPECT_EQ(
1, user_action_tester.GetActionCount("LinkCapturingIPHAppBubbleShown"));
chrome::CloseWindow(app_browser);
ui_test_utils::WaitForBrowserToClose(app_browser);
EXPECT_EQ(1, user_action_tester.GetActionCount(
"LinkCapturingIPHAppBubbleNotAccepted"));
}
IN_PROC_BROWSER_TEST_P(WebAppLinkCapturingIPHPromoTest,
AcceptingBubbleMeasuresUserAccept) {
const webapps::AppId app_id = InstallApp();
base::UserActionTester user_action_tester;
SetUpSiteForLinkCapturingIphBubble(app_id);
Browser* app_browser = TriggerAppLaunchIphAndGetBrowser(app_id);
EXPECT_EQ(
1, user_action_tester.GetActionCount("LinkCapturingIPHAppBubbleShown"));
AcceptCustomActionIPH(app_browser);
EXPECT_EQ(1, user_action_tester.GetActionCount(
"LinkCapturingIPHAppBubbleAccepted"));
}
IN_PROC_BROWSER_TEST_P(WebAppLinkCapturingIPHPromoTest,
BubbleDismissMeasuresUserDismiss) {
const webapps::AppId app_id = InstallApp();
base::UserActionTester user_action_tester;
SetUpSiteForLinkCapturingIphBubble(app_id);
Browser* app_browser = TriggerAppLaunchIphAndGetBrowser(app_id);
DismissIPH(app_browser);
EXPECT_EQ(1, user_action_tester.GetActionCount(
"LinkCapturingIPHAppBubbleNotAccepted"));
}
INSTANTIATE_TEST_SUITE_P(All,
WebAppLinkCapturingIPHPromoTest,
testing::Values(true, false),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "LinkCapturingEnabled"
: "LinkCapturingDisabled";
});