chromium/chrome/browser/ui/ash/shelf/app_service/app_service_shelf_context_menu_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 "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_item_delegate.h"
#include "ash/public/cpp/shelf_model.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/stack_allocated.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/app/vector_icons/vector_icons.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/bruschetta/bruschetta_service.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_test_helper.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_terminal.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/browser.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/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "third_party/blink/public/common/features.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/display/display.h"
#include "ui/views/vector_icons.h"

class AppServiceShelfContextMenuBrowserTest : public InProcessBrowserTest {
 public:
  AppServiceShelfContextMenuBrowserTest() = default;

  ~AppServiceShelfContextMenuBrowserTest() override = default;

  struct MenuSection {
    STACK_ALLOCATED();

   public:
    std::unique_ptr<ui::SimpleMenuModel> menu_model;
    ui::MenuModel* sub_model = nullptr;
    size_t command_index = 0;
  };

  std::optional<MenuSection> GetContextMenuSectionForAppCommand(
      const webapps::AppId& app_id,
      int command_id) {
    MenuSection result;
    ash::ShelfModel* shelf_model = ash::ShelfModel::Get();
    PinAppWithIDToShelf(app_id);
    ash::ShelfItemDelegate* delegate =
        shelf_model->GetShelfItemDelegate(ash::ShelfID(app_id));
    base::RunLoop run_loop;
    delegate->GetContextMenu(
        display::Display::GetDefaultDisplay().id(),
        base::BindLambdaForTesting(
            [&run_loop, &result](std::unique_ptr<ui::SimpleMenuModel> model) {
              result.menu_model = std::move(model);
              run_loop.Quit();
            }));
    run_loop.Run();

    result.sub_model = result.menu_model.get();
    result.command_index = 0;
    if (!ui::MenuModel::GetModelAndIndexForCommandId(
            command_id, &result.sub_model, &result.command_index)) {
      return std::nullopt;
    }

    return result;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class AppServiceShelfContextMenuWebAppBrowserTest
    : public AppServiceShelfContextMenuBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  AppServiceShelfContextMenuWebAppBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        {blink::features::kDesktopPWAsTabStrip,
         features::kDesktopPWAsTabStripSettings},
        {});
  }
  ~AppServiceShelfContextMenuWebAppBrowserTest() override = default;

  const gfx::VectorIcon& GetExpectedLaunchNewIcon(int command_id) {
    if (command_id == ash::USE_LAUNCH_TYPE_REGULAR)
      return views::kNewTabIcon;
    else if (command_id == ash::USE_LAUNCH_TYPE_WINDOW)
      return views::kNewWindowIcon;
    else
      return views::kOpenIcon;
  }

  bool IsShortstandEnabled() { return GetParam(); }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_P(AppServiceShelfContextMenuWebAppBrowserTest,
                       WindowCommandCheckedForMinimalUi) {
  Profile* profile = browser()->profile();
  base::UserActionTester user_action_tester;

  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  web_app_install_info->display_mode = blink::mojom::DisplayMode::kMinimalUi;
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  // When Shortstand is enabled, the display mode can no longer be changed
  // through the context menu. The submenu is replaced with a 'New Window'
  // command.
  if (IsShortstandEnabled()) {
    std::optional<MenuSection> menu_section =
        GetContextMenuSectionForAppCommand(app_id, ash::LAUNCH_NEW);
    ASSERT_TRUE(menu_section);
    return;
  }

  // Activate open in window menu item.
  std::optional<MenuSection> menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::USE_LAUNCH_TYPE_WINDOW);
  ASSERT_TRUE(menu_section);
  menu_section->sub_model->ActivatedAt(menu_section->command_index);
  web_app::WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();

  EXPECT_EQ(user_action_tester.GetActionCount("WebApp.SetWindowMode.Window"),
            1);

  // Open in window should be checked after activating it.
  EXPECT_TRUE(
      menu_section->sub_model->IsItemCheckedAt(menu_section->command_index));
}

IN_PROC_BROWSER_TEST_P(AppServiceShelfContextMenuWebAppBrowserTest,
                       SetOpenInTabbedWindow) {
  // As the display mode can no longer be changed through the context menu when
  // Shortstand is enabled, this test is skipped.
  if (IsShortstandEnabled()) {
    GTEST_SKIP();
  }

  Profile* profile = browser()->profile();
  base::UserActionTester user_action_tester;

  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  web_app_install_info->display_mode = blink::mojom::DisplayMode::kMinimalUi;
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  // Set app to open in tabbed window.
  std::optional<MenuSection> menu_section = GetContextMenuSectionForAppCommand(
      app_id, ash::USE_LAUNCH_TYPE_TABBED_WINDOW);
  ASSERT_TRUE(menu_section);
  menu_section->sub_model->ActivatedAt(menu_section->command_index);
  web_app::WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();

  EXPECT_EQ(user_action_tester.GetActionCount("WebApp.SetWindowMode.Tabbed"),
            1);

  // App window should have tab strip.
  Browser* app_browser = web_app::LaunchWebAppBrowser(profile, app_id);
  EXPECT_TRUE(app_browser->app_controller()->has_tab_strip());
}

IN_PROC_BROWSER_TEST_P(AppServiceShelfContextMenuWebAppBrowserTest,
                       SetOpenInBrowserTab) {
  // As the display mode can no longer be changed through the context menu when
  // Shortstand is enabled, this test is skipped.
  if (IsShortstandEnabled()) {
    GTEST_SKIP();
  }
  Profile* profile = browser()->profile();
  base::UserActionTester user_action_tester;

  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  web_app_install_info->user_display_mode =
      web_app::mojom::UserDisplayMode::kStandalone;
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  // Set app to open in browser tab.
  std::optional<MenuSection> menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::USE_LAUNCH_TYPE_REGULAR);
  ASSERT_TRUE(menu_section);
  menu_section->sub_model->ActivatedAt(menu_section->command_index);
  web_app::WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();

  EXPECT_EQ(user_action_tester.GetActionCount("WebApp.SetWindowMode.Tab"), 1);
}

IN_PROC_BROWSER_TEST_P(AppServiceShelfContextMenuWebAppBrowserTest,
                       LaunchNewMenuItemDynamicallyChanges) {
  // As the display mode can no longer be changed through the context menu when
  // Shortstand is enabled, this test is skipped.
  if (IsShortstandEnabled()) {
    GTEST_SKIP();
  }

  Profile* profile = browser()->profile();
  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  std::optional<MenuSection> menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::LAUNCH_NEW);
  ASSERT_TRUE(menu_section);

  auto* launch_new_submodel =
      menu_section->menu_model->GetSubmenuModelAt(menu_section->command_index);

  EXPECT_GT(launch_new_submodel->GetItemCount(), 0u);
  for (size_t launch_new_item_index = 0;
       launch_new_item_index < launch_new_submodel->GetItemCount();
       ++launch_new_item_index) {
    const auto label_from_submenu =
        launch_new_submodel->GetLabelAt(launch_new_item_index);
    launch_new_submodel->ActivatedAt(launch_new_item_index);
    web_app::WebAppProvider::GetForTest(profile)
        ->command_manager()
        .AwaitAllCommandsCompleteForTesting();
    EXPECT_TRUE(launch_new_submodel->IsItemCheckedAt(launch_new_item_index));

    // Parent `LAUNCH_NEW` item label and icon change dynamically after
    // selection.
    EXPECT_EQ(menu_section->menu_model->GetLabelAt(menu_section->command_index),
              label_from_submenu);
    EXPECT_EQ(menu_section->menu_model->GetIconAt(menu_section->command_index)
                  .GetVectorIcon()
                  .vector_icon(),
              &GetExpectedLaunchNewIcon(
                  launch_new_submodel->GetCommandIdAt(launch_new_item_index)));
  }
}
INSTANTIATE_TEST_SUITE_P(All,
                         AppServiceShelfContextMenuWebAppBrowserTest,
                         ::testing::Bool());

class AppServiceShelfContextMenuTabbedWebAppBrowserTest
    : public AppServiceShelfContextMenuBrowserTest {
 public:
  AppServiceShelfContextMenuTabbedWebAppBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        {blink::features::kDesktopPWAsTabStrip},
        {features::kDesktopPWAsTabStripSettings});
  }
  ~AppServiceShelfContextMenuTabbedWebAppBrowserTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(AppServiceShelfContextMenuTabbedWebAppBrowserTest,
                       SetOpenInWindow) {
  Profile* profile = browser()->profile();
  base::UserActionTester user_action_tester;

  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  web_app_install_info->display_mode = blink::mojom::DisplayMode::kStandalone;
  web_app_install_info->display_override = {blink::mojom::DisplayMode::kTabbed};
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  // Select the "Open in window" menu item.
  std::optional<MenuSection> menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::USE_LAUNCH_TYPE_WINDOW);
  ASSERT_TRUE(menu_section);
  menu_section->sub_model->ActivatedAt(menu_section->command_index);
  web_app::WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(menu_section->sub_model->IsItemCheckedAt(1));

  EXPECT_EQ(user_action_tester.GetActionCount("WebApp.SetWindowMode.Window"),
            1);

  // App window should have tab strip.
  Browser* app_browser = web_app::LaunchWebAppBrowser(profile, app_id);
  EXPECT_TRUE(app_browser->app_controller()->has_tab_strip());
}

class AppServiceShelfContextMenuNonTabbedWebAppBrowserTest
    : public AppServiceShelfContextMenuBrowserTest {
 public:
  AppServiceShelfContextMenuNonTabbedWebAppBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        {}, {blink::features::kDesktopPWAsTabStrip,
             features::kDesktopPWAsTabStripSettings});
  }
  ~AppServiceShelfContextMenuNonTabbedWebAppBrowserTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(AppServiceShelfContextMenuNonTabbedWebAppBrowserTest,
                       SetOpenInWindow) {
  Profile* profile = browser()->profile();
  base::UserActionTester user_action_tester;

  auto web_app_install_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
          GURL("https://example.org"));
  web_app_install_info->display_mode = blink::mojom::DisplayMode::kStandalone;
  web_app_install_info->display_override = {blink::mojom::DisplayMode::kTabbed};
  webapps::AppId app_id =
      web_app::test::InstallWebApp(profile, std::move(web_app_install_info));

  // Select the "Open in window" menu item.
  std::optional<MenuSection> menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::USE_LAUNCH_TYPE_WINDOW);
  ASSERT_TRUE(menu_section);
  menu_section->sub_model->ActivatedAt(menu_section->command_index);
  web_app::WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(menu_section->sub_model->IsItemCheckedAt(1));

  EXPECT_EQ(user_action_tester.GetActionCount("WebApp.SetWindowMode.Window"),
            1);

  // App window should not have a tab strip since the flag is disabled.
  Browser* app_browser = web_app::LaunchWebAppBrowser(profile, app_id);
  EXPECT_FALSE(app_browser->app_controller()->has_tab_strip());
}

class AppServiceShelfContextMenuCrostiniAppBrowserTest
    : public AppServiceShelfContextMenuBrowserTest {
 public:
  AppServiceShelfContextMenuCrostiniAppBrowserTest() = default;
  ~AppServiceShelfContextMenuCrostiniAppBrowserTest() override = default;

  std::string InstallCrostiniApp() {
    vm_tools::apps::ApplicationList crostini_list;
    crostini_list.set_vm_name(crostini::kCrostiniDefaultVmName);
    crostini_list.set_container_name(crostini::kCrostiniDefaultContainerName);
    *crostini_list.add_apps() = crostini::CrostiniTestHelper::BasicApp(
        "app-service-context-menu-test-app");

    guest_os::GuestOsRegistryServiceFactory::GetForProfile(browser()->profile())
        ->UpdateApplicationList(crostini_list);

    return crostini::CrostiniTestHelper::GenerateAppId(
        "app-service-context-menu-test-app", crostini::kCrostiniDefaultVmName,
        crostini::kCrostiniDefaultContainerName);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_{features::kCrostini};
};

// Crostini apps have `LAUNCH_NEW` menu item at non-0 position without submenu.
// Make sure there is no crash.
IN_PROC_BROWSER_TEST_F(AppServiceShelfContextMenuCrostiniAppBrowserTest,
                       LaunchNewForCrostiniApps) {
  auto app_id = InstallCrostiniApp();
  auto menu_section =
      GetContextMenuSectionForAppCommand(app_id, ash::LAUNCH_NEW);
  ASSERT_TRUE(menu_section);

  EXPECT_GT(menu_section->command_index, 0u);
  EXPECT_FALSE(
      menu_section->menu_model->GetSubmenuModelAt(menu_section->command_index));
  EXPECT_EQ(menu_section->menu_model->GetLabelAt(menu_section->command_index),
            u"Open");
  EXPECT_EQ(menu_section->menu_model->GetIconAt(menu_section->command_index)
                .GetVectorIcon()
                .vector_icon(),
            &views::kOpenIcon);
}

IN_PROC_BROWSER_TEST_F(AppServiceShelfContextMenuCrostiniAppBrowserTest,
                       ShutDownGuestOs) {
  ash::SystemWebAppManager::Get(browser()->profile())
      ->InstallSystemAppsForTesting();
  auto menu_section = GetContextMenuSectionForAppCommand(
      guest_os::kTerminalSystemAppId, ash::SHUTDOWN_GUEST_OS);
  ASSERT_FALSE(menu_section);

  auto* crostini_manager =
      crostini::CrostiniManager::GetForProfile(browser()->profile());
  crostini_manager->AddRunningVmForTesting(crostini::kCrostiniDefaultVmName);
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();

  menu_section = GetContextMenuSectionForAppCommand(
      guest_os::kTerminalSystemAppId, ash::SHUTDOWN_GUEST_OS);
  ASSERT_TRUE(menu_section);

  EXPECT_GT(menu_section->command_index, 0u);
  EXPECT_FALSE(
      menu_section->menu_model->GetSubmenuModelAt(menu_section->command_index));
  EXPECT_EQ(menu_section->menu_model->GetLabelAt(menu_section->command_index),
            u"Shut down Linux");
  EXPECT_EQ(menu_section->menu_model->GetIconAt(menu_section->command_index)
                .GetVectorIcon()
                .vector_icon(),
            &kShutdownGuestOsIcon);
}

IN_PROC_BROWSER_TEST_F(AppServiceShelfContextMenuCrostiniAppBrowserTest,
                       ShutDownBruschettaOs) {
  ash::SystemWebAppManager::Get(browser()->profile())
      ->InstallSystemAppsForTesting();
  auto menu_section = GetContextMenuSectionForAppCommand(
      guest_os::kTerminalSystemAppId, ash::SHUTDOWN_BRUSCHETTA_OS);
  ASSERT_FALSE(menu_section);

  guest_os::GuestId id(guest_os::VmType::BRUSCHETTA,
                       bruschetta::kBruschettaVmName, "");
  guest_os::GuestOsSessionTracker::GetForProfile(browser()->profile())
      ->AddGuestForTesting(id, guest_os::GuestInfo{id, 0, {}, {}, {}, {}});

  auto* bruschetta_service =
      bruschetta::BruschettaService::GetForProfile(browser()->profile());
  bruschetta_service->RegisterVmLaunch(bruschetta::kBruschettaVmName,
                                       bruschetta::RunningVmPolicy{false});
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();

  menu_section = GetContextMenuSectionForAppCommand(
      guest_os::kTerminalSystemAppId, ash::SHUTDOWN_BRUSCHETTA_OS);
  ASSERT_TRUE(menu_section);

  EXPECT_GT(menu_section->command_index, 0u);
  EXPECT_FALSE(
      menu_section->menu_model->GetSubmenuModelAt(menu_section->command_index));
  EXPECT_NE(menu_section->menu_model->GetLabelAt(menu_section->command_index)
                .find(base::ASCIIToUTF16(bruschetta::GetBruschettaDisplayName(
                    browser()->profile()))),
            std::string::npos);
  EXPECT_EQ(menu_section->menu_model->GetIconAt(menu_section->command_index)
                .GetVectorIcon()
                .vector_icon(),
            &kShutdownGuestOsIcon);
}