chromium/chrome/browser/ui/views/apps/app_dialog/app_uninstall_dialog_view_browsertest.cc

// 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 "chrome/browser/ui/views/apps/app_dialog/app_uninstall_dialog_view.h"

#include <optional>
#include <string>

#include "ash/components/arc/test/arc_util_test_support.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_app_instance.h"
#include "base/containers/to_vector.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/types/cxx23_to_underlying.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/app_list/arc/arc_app_icon.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/test/test_browser_dialog.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/views/view.h"
#include "ui/views/widget/any_widget_observer.h"

class AppUninstallDialogViewBrowserTest : public DialogBrowserTest {
 public:
  AppUninstallDialogView* ActiveView() {
    return AppUninstallDialogView::GetActiveViewForTesting();
  }

  std::optional<std::u16string> GetTitleText(
      AppUninstallDialogView* uninstall_dialog_view) {
    return uninstall_dialog_view->GetTitleTextForTesting();
  }

  void ShowUi(const std::string& name) override {
    EXPECT_EQ(nullptr, ActiveView());

    auto* app_service_proxy =
        apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
    ASSERT_TRUE(app_service_proxy);

    UninstallApp(app_id_);

    ASSERT_NE(nullptr, ActiveView());
    EXPECT_EQ(static_cast<int>(ui::mojom::DialogButton::kOk) |
                  static_cast<int>(ui::mojom::DialogButton::kCancel),
              ActiveView()->buttons());
    std::u16string title =
        u"Uninstall \"" + base::ASCIIToUTF16(app_name_) + u"\"?";
    EXPECT_EQ(title, GetTitleText(ActiveView()));

    if (name == "accept") {
      if (app_service_proxy->AppRegistryCache().GetAppType(app_id_) ==
          apps::AppType::kWeb) {
        web_app::WebAppTestUninstallObserver app_listener(browser()->profile());
        app_listener.BeginListening();
        ActiveView()->AcceptDialog();
        app_listener.Wait();
      } else {
        ActiveView()->AcceptDialog();
      }

      bool is_uninstalled = false;
      app_service_proxy->AppRegistryCache().ForOneApp(
          app_id_, [&is_uninstalled, name](const apps::AppUpdate& update) {
            is_uninstalled =
                (update.Readiness() == apps::Readiness::kUninstalledByUser);
          });

      EXPECT_TRUE(is_uninstalled);
    } else {
      ActiveView()->CancelDialog();

      bool is_installed = true;
      app_service_proxy->AppRegistryCache().ForOneApp(
          app_id_, [&is_installed, name](const apps::AppUpdate& update) {
            is_installed = (update.Readiness() == apps::Readiness::kReady);
          });

      EXPECT_TRUE(is_installed);
    }
    // Wait for the dialog window to be closed to destroy the Uninstall
    // dialog.
    base::RunLoop().RunUntilIdle();
    EXPECT_EQ(nullptr, ActiveView());
  }

  // Uninstalls the given app, and returns true if the confirmation dialog was
  // displayed.
  bool UninstallApp(std::string app_id) {
    auto* app_service_proxy =
        apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
    DCHECK(app_service_proxy);

    base::test::TestFuture<bool> future;
    app_service_proxy->UninstallForTesting(app_id, nullptr,
                                           future.GetCallback());
    return future.Get();
  }

 protected:
  std::string app_id_;
  std::string app_name_;
};

class ArcAppsUninstallDialogViewBrowserTest
    : public AppUninstallDialogViewBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    arc::SetArcAvailableCommandLineForTesting(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    arc::ArcSessionManager::SetUiEnabledForTesting(false);
  }

  void SetUpOnMainThread() override {
    AppUninstallDialogViewBrowserTest::SetUpOnMainThread();

    arc::SetArcPlayStoreEnabledForProfile(browser()->profile(), true);

    // Validating decoded content does not fit well for unit tests.
    ArcAppIcon::DisableSafeDecodingForTesting();

    arc_app_list_pref_ = ArcAppListPrefs::Get(browser()->profile());
    ASSERT_TRUE(arc_app_list_pref_);
    base::RunLoop run_loop;
    arc_app_list_pref_->SetDefaultAppsReadyCallback(run_loop.QuitClosure());
    run_loop.Run();

    app_instance_ = std::make_unique<arc::FakeAppInstance>(arc_app_list_pref_);
    arc_app_list_pref_->app_connection_holder()->SetInstance(
        app_instance_.get());
    WaitForInstanceReady(arc_app_list_pref_->app_connection_holder());
  }

  void TearDownOnMainThread() override {
    arc_app_list_pref_->app_connection_holder()->CloseInstance(
        app_instance_.get());
    app_instance_.reset();
    arc::ArcSessionManager::Get()->Shutdown();
  }

  void CreateApp() {
    std::vector<arc::mojom::AppInfoPtr> apps;
    apps.emplace_back(arc::mojom::AppInfo::New("Fake App 0", "fake.package.0",
                                               "fake.app.0.activity",
                                               false /* sticky */));
    app_instance_->SendRefreshAppList(apps);
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(1u, arc_app_list_pref_->GetAppIds().size());
    app_id_ =
        arc_app_list_pref_->GetAppId(apps[0]->package_name, apps[0]->activity);
    app_name_ = apps[0]->name;
  }

 private:
  raw_ptr<ArcAppListPrefs, DanglingUntriaged> arc_app_list_pref_ = nullptr;
  std::unique_ptr<arc::FakeAppInstance> app_instance_;
};

IN_PROC_BROWSER_TEST_F(ArcAppsUninstallDialogViewBrowserTest, InvokeUi_Accept) {
  CreateApp();
  ShowUi("accept");
}

IN_PROC_BROWSER_TEST_F(ArcAppsUninstallDialogViewBrowserTest, InvokeUi_Cancel) {
  CreateApp();
  ShowUi("cancel");
}

class WebAppsUninstallDialogViewBrowserTest
    : public AppUninstallDialogViewBrowserTest {
 public:
  void SetUpOnMainThread() override {
    AppUninstallDialogViewBrowserTest::SetUpOnMainThread();

    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(https_server_.Start());
  }

  void CreateApp() {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GetAppURL());
    web_app_info->scope = GetAppURL().GetWithoutFilename();

    app_id_ = web_app::test::InstallWebApp(browser()->profile(),
                                           std::move(web_app_info));
    content::TestNavigationObserver navigation_observer(GetAppURL());
    navigation_observer.StartWatchingNewWebContents();
    web_app::LaunchWebAppBrowser(browser()->profile(), app_id_);
    navigation_observer.WaitForNavigationFinished();

    auto* provider = web_app::WebAppProvider::GetForTest(browser()->profile());
    DCHECK(provider);
    app_name_ = provider->registrar_unsafe().GetAppShortName(app_id_);
  }

  GURL GetAppURL() const {
    return https_server_.GetURL("app.com", "/ssl/google.html");
  }

 protected:
  // For mocking a secure site.
  net::EmbeddedTestServer https_server_;
};

IN_PROC_BROWSER_TEST_F(WebAppsUninstallDialogViewBrowserTest, InvokeUi_Accept) {
  CreateApp();
  ShowUi("accept");
}

IN_PROC_BROWSER_TEST_F(WebAppsUninstallDialogViewBrowserTest, InvokeUi_Cancel) {
  CreateApp();
  ShowUi("cancel");
}

IN_PROC_BROWSER_TEST_F(WebAppsUninstallDialogViewBrowserTest,
                       ExistingDialogFocus) {
  CreateApp();

  // First call to uninstall should return true in callback for successful.
  EXPECT_TRUE(UninstallApp(app_id_));

  views::Widget* first_widget = ActiveView()->GetWidget();
  first_widget->Hide();
  EXPECT_FALSE(first_widget->IsVisible());

  // Second call should be unsuccessful.
  // The shown widget should be the one opened in the first call to uninstall.
  views::AnyWidgetObserver observer(views::test::AnyWidgetTestPasskey{});
  observer.set_shown_callback(
      base::BindLambdaForTesting([&](views::Widget* widget) {
        EXPECT_EQ(first_widget, widget);
        EXPECT_TRUE(first_widget->IsVisible());
      }));
  EXPECT_FALSE(UninstallApp(app_id_));
}

IN_PROC_BROWSER_TEST_F(WebAppsUninstallDialogViewBrowserTest,
                       PreventDuplicateUninstallDialogs) {
  CreateApp();

  EXPECT_EQ(nullptr, ActiveView());

  auto* app_service_proxy =
      apps::AppServiceProxyFactory::GetForProfile(browser()->profile());
  ASSERT_TRUE(app_service_proxy);

  // First call to uninstall should return true in callback for successful.
  EXPECT_TRUE(UninstallApp(app_id_));

  // Second call should be unsuccessful.
  EXPECT_FALSE(UninstallApp(app_id_));

  ASSERT_NE(nullptr, ActiveView());
  EXPECT_EQ(static_cast<int>(ui::mojom::DialogButton::kOk) |
                static_cast<int>(ui::mojom::DialogButton::kCancel),
            ActiveView()->buttons());
  std::u16string title =
      u"Uninstall \"" + base::ASCIIToUTF16(app_name_) + u"\"?";
  EXPECT_EQ(title, GetTitleText(ActiveView()));

  // Cancelling the active dialog should not uninstall the web app.
  ActiveView()->CancelDialog();
  // Wait for the dialog window to be closed to destroy the Uninstall dialog.
  base::RunLoop().RunUntilIdle();

  bool is_installed = true;
  app_service_proxy->AppRegistryCache().ForOneApp(
      app_id_, [&is_installed](const apps::AppUpdate& update) {
        is_installed = update.Readiness() == apps::Readiness::kReady;
      });
  EXPECT_TRUE(is_installed);

  // Uninstall dialog should be reopenable.
  EXPECT_EQ(nullptr, ActiveView());
  EXPECT_TRUE(UninstallApp(app_id_));
  ASSERT_NE(nullptr, ActiveView());
}

class IsolatedWebAppsUninstallDialogViewBrowserTest
    : public AppUninstallDialogViewBrowserTest {
 public:
  IsolatedWebAppsUninstallDialogViewBrowserTest() {
    feature_list_.InitWithFeatures(
        {features::kIsolatedWebApps, features::kIsolatedWebAppDevMode}, {});
  }

 protected:
  base::test::ScopedFeatureList feature_list_;

  std::vector<views::View*> GetViews(
      views::View* view,
      AppUninstallDialogView::DialogViewID view_type) {
    std::vector<raw_ptr<views::View, VectorExperimental>> views_group;

    view->GetViewsInGroup(base::to_underlying(view_type), &views_group);
    return base::ToVector(views_group, [](auto item) { return item.get(); });
  }
};

IN_PROC_BROWSER_TEST_F(IsolatedWebAppsUninstallDialogViewBrowserTest,
                       SubAppsShownCorrectly) {
  std::unique_ptr<net::EmbeddedTestServer> iwa_dev_server =
      web_app::CreateAndStartDevServer(
          FILE_PATH_LITERAL("web_apps/subapps_isolated_app"));

  web_app::IsolatedWebAppUrlInfo parent_app =
      web_app::InstallDevModeProxyIsolatedWebApp(browser()->profile(),
                                                 iwa_dev_server->GetOrigin());
  const webapps::AppId parent_app_id = parent_app.app_id();
  const GURL parent_app_url = parent_app.origin().GetURL();

  std::unordered_set<std::u16string> sub_apps_expected;

  // Include non-ASCII characters in app names to ensure they get displayed
  // correctly
  for (const std::string& app_name : {"one", "fünf", "🌈"}) {
    std::u16string sub_app_name = u"Sub App " + base::UTF8ToUTF16(app_name);
    sub_apps_expected.emplace(sub_app_name);

    GURL start_url = parent_app_url.Resolve("/sub-app-" + app_name);
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
    web_app_info->parent_app_id = parent_app_id;
    web_app_info->title = sub_app_name;
    web_app_info->parent_app_manifest_id = parent_app_url;

    web_app::test::InstallWebApp(browser()->profile(), std::move(web_app_info),
                                 /*overwrite_existing_manifest_fields=*/true,
                                 webapps::WebappInstallSource::SUB_APP);
  }

  EXPECT_TRUE(UninstallApp(parent_app_id));

  views::View* view = ActiveView()->GetWidget()->GetContentsView();
  std::vector<views::View*> views_group;

  ASSERT_TRUE(base::test::RunUntil([&]() {
    views_group =
        GetViews(view, AppUninstallDialogView::DialogViewID::SUB_APP_LABEL);
    return views_group.size() == 3u;
  }));

  std::unordered_set<std::u16string> sub_apps_actual;
  for (views::View* label : views_group) {
    sub_apps_actual.emplace(static_cast<views::Label*>(label)->GetText());
  }
  EXPECT_EQ(sub_apps_actual, sub_apps_expected);

  std::vector<views::View*> sub_app_icons =
      GetViews(view, AppUninstallDialogView::DialogViewID::SUB_APP_ICON);
  EXPECT_EQ(sub_app_icons.size(), 3u);
  views::ImageView* icon_view =
      static_cast<views::ImageView*>(sub_app_icons[0]);
  EXPECT_FALSE(icon_view->GetImageModel().IsEmpty());
  EXPECT_EQ(icon_view->GetVisibleBounds().size(), gfx::Size(32, 32));
}

IN_PROC_BROWSER_TEST_F(IsolatedWebAppsUninstallDialogViewBrowserTest,
                       SubAppsAreNotShownWhenNoneAreInstalled) {
  std::unique_ptr<net::EmbeddedTestServer> iwa_dev_server =
      web_app::CreateAndStartDevServer(
          FILE_PATH_LITERAL("web_apps/subapps_isolated_app"));

  web_app::IsolatedWebAppUrlInfo parent_app =
      web_app::InstallDevModeProxyIsolatedWebApp(browser()->profile(),
                                                 iwa_dev_server->GetOrigin());
  const webapps::AppId parent_app_id = parent_app.app_id();
  const GURL parent_app_url = parent_app.origin().GetURL();

  std::unordered_set<std::u16string> sub_apps_expected;

  EXPECT_TRUE(UninstallApp(parent_app_id));

  views::View* view = ActiveView()->GetWidget()->GetContentsView();
  ASSERT_TRUE(base::test::RunUntil([&]() { return view->GetVisible(); }));

  // the subapps list should be not visible
  std::vector<views::View*> views_group =
      GetViews(view, AppUninstallDialogView::DialogViewID::SUB_APP_LABEL);
  EXPECT_THAT(views_group, testing::IsEmpty());
}