chromium/chrome/browser/ash/app_list/app_context_menu_unittest.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <map>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>

#include "ash/components/arc/test/fake_app_instance.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "base/functional/bind.h"
#include "base/json/json_file_value_serializer.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.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/apps/app_service/app_service_test.h"
#include "chrome/browser/ash/app_list/app_context_menu_delegate.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_list_test_util.h"
#include "chrome/browser/ash/app_list/app_service/app_service_app_item.h"
#include "chrome/browser/ash/app_list/app_service/app_service_context_menu.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_app_test.h"
#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
#include "chrome/browser/ash/app_list/internal_app/internal_app_metadata.h"
#include "chrome/browser/ash/app_list/test/fake_app_list_model_updater.h"
#include "chrome/browser/ash/app_list/test/test_app_list_controller_delegate.h"
#include "chrome/browser/ash/arc/icon_decode_request.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/menu_manager_factory.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/standalone_browser/feature_refs.h"
#include "chromeos/ash/components/standalone_browser/migrator_util.h"
#include "components/app_constants/constants.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/user_manager/scoped_user_manager.h"
#include "extensions/common/manifest_constants.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/test/test_screen.h"

namespace app_list {

namespace {

class FakeAppContextMenuDelegate : public AppContextMenuDelegate {
 public:
  FakeAppContextMenuDelegate() = default;
  FakeAppContextMenuDelegate(const FakeAppContextMenuDelegate&) = delete;
  FakeAppContextMenuDelegate& operator=(const FakeAppContextMenuDelegate&) =
      delete;
  ~FakeAppContextMenuDelegate() override = default;

  // AppContextMenuDelegate overrides:
  void ExecuteLaunchCommand(int event_flags) override {}
};

class FakeAppListControllerDelegate
    : public ::test::TestAppListControllerDelegate {
 public:
  FakeAppListControllerDelegate() = default;
  FakeAppListControllerDelegate(const FakeAppListControllerDelegate&) = delete;
  FakeAppListControllerDelegate& operator=(
      const FakeAppListControllerDelegate&) = delete;
  ~FakeAppListControllerDelegate() override = default;

  void SetAppPinnable(const std::string& app_id, Pinnable type) {
    pinnable_apps_[app_id] = type;
  }

  void SetAppOpen(const std::string& app_id, bool open) {
    if (open)
      open_apps_.insert(app_id);
    else
      open_apps_.erase(app_id);
  }

  bool IsAppOpen(const std::string& app_id) const override {
    return open_apps_.count(app_id) != 0;
  }

  // test::TestAppListControllerDelegate overrides:
  Pinnable GetPinnable(const std::string& app_id) override {
    std::map<std::string, Pinnable>::const_iterator it;
    it = pinnable_apps_.find(app_id);
    if (it == pinnable_apps_.end())
      return NO_PIN;
    return it->second;
  }

 private:
  std::map<std::string, Pinnable> pinnable_apps_;
  std::unordered_set<std::string> open_apps_;
};

class FakeAppServiceAppItem : public AppServiceAppItem {
 public:
  FakeAppServiceAppItem(Profile* profile,
                        AppListModelUpdater* model_updater,
                        const AppListSyncableService::SyncItem* sync_item,
                        const apps::AppUpdate& app_update)
      : AppServiceAppItem(profile, model_updater, sync_item, app_update) {}
  FakeAppServiceAppItem(const FakeAppServiceAppItem&) = delete;
  FakeAppServiceAppItem& operator=(const FakeAppServiceAppItem&) = delete;
  ~FakeAppServiceAppItem() override = default;

  // AppContextMenuDelegate overrides:
  void ExecuteLaunchCommand(int event_flags) override {
    AppServiceAppItem::ExecuteLaunchCommand(event_flags);

    if (!quit_callback_.is_null())
      std::move(quit_callback_).Run();
  }

  void WaitForLaunch() {
    base::RunLoop run_loop;
    quit_callback_ = run_loop.QuitClosure();
    run_loop.Run();
  }

 private:
  base::OnceClosure quit_callback_;
};

std::unique_ptr<KeyedService> MenuManagerFactory(
    content::BrowserContext* context) {
  return extensions::MenuManagerFactory::BuildServiceInstanceForTesting(
      context);
}

std::unique_ptr<FakeAppServiceAppItem> GetAppListItem(
    Profile* profile,
    const std::string& app_id) {
  std::unique_ptr<FakeAppServiceAppItem> item;
  apps::AppServiceProxyFactory::GetForProfile(profile)
      ->AppRegistryCache()
      .ForOneApp(app_id, [profile, &item](const apps::AppUpdate& update) {
        item = std::make_unique<FakeAppServiceAppItem>(
            profile, /*model_updater=*/nullptr, /*sync_item=*/nullptr, update);

        // Because model updater is null, set position manually.
        item->SetChromePosition(item->CalculateDefaultPositionForTest());
      });
  return item;
}

std::unique_ptr<ui::SimpleMenuModel> GetContextMenuModel(
    ChromeAppListItem* item) {
  base::RunLoop run_loop;
  std::unique_ptr<ui::SimpleMenuModel> menu;
  item->GetContextMenuModel(
      ash::AppListItemContext::kNone,
      base::BindLambdaForTesting(
          [&](std::unique_ptr<ui::SimpleMenuModel> created_menu) {
            menu = std::move(created_menu);
            run_loop.Quit();
          }));
  run_loop.Run();
  return menu;
}

std::unique_ptr<ui::SimpleMenuModel> GetMenuModel(
    AppContextMenu* context_menu) {
  base::RunLoop run_loop;
  std::unique_ptr<ui::SimpleMenuModel> menu;
  context_menu->GetMenuModel(base::BindLambdaForTesting(
      [&](std::unique_ptr<ui::SimpleMenuModel> created_menu) {
        menu = std::move(created_menu);
        run_loop.Quit();
      }));
  run_loop.Run();
  return menu;
}

}  // namespace

class AppContextMenuTest : public AppListTestBase {
 public:
  AppContextMenuTest() { display::Screen::SetScreenInstance(&test_screen_); }
  AppContextMenuTest(const AppContextMenuTest&) = delete;
  AppContextMenuTest& operator=(const AppContextMenuTest&) = delete;
  ~AppContextMenuTest() override {
    display::Screen::SetScreenInstance(nullptr);
  }

  void SetUp() override {
    AppListTestBase::SetUp();
    extensions::MenuManagerFactory::GetInstance()->SetTestingFactory(
        profile(), base::BindRepeating(&MenuManagerFactory));
    controller_ = std::make_unique<FakeAppListControllerDelegate>();
    menu_delegate_ = std::make_unique<FakeAppContextMenuDelegate>();
    model_updater_ = std::make_unique<FakeAppListModelUpdater>(
        /*profile=*/nullptr, /*reorder_delegate=*/nullptr);
    ChromeAppListItem::OverrideAppListControllerDelegateForTesting(
        controller());
  }

  void TearDown() override {
    // Let any in-flight tasks finish, e.g. clear the background thread icon
    // decode, otherwise the test might flake (crbug.com/1115763).
    base::RunLoop().RunUntilIdle();
    menu_delegate_.reset();
    controller_.reset();
    menu_manager_.reset();
  }

 protected:
  struct MenuState {
    // Defines separator.
    MenuState() : command_id(-1), is_enabled(true), is_checked(false) {}

    // Defines enabled unchecked command.
    explicit MenuState(int command_id)
        : command_id(command_id), is_enabled(true), is_checked(false) {}

    MenuState(int command_id, bool enabled, bool checked)
        : command_id(command_id), is_enabled(enabled), is_checked(checked) {}

    int command_id;
    bool is_enabled;
    bool is_checked;
  };

  void ValidateItemState(const ui::MenuModel* menu_model,
                         size_t index,
                         const MenuState& state) {
    EXPECT_EQ(state.command_id, menu_model->GetCommandIdAt(index));
    if (state.command_id == -1)
      return;  // Don't check separator.
    EXPECT_EQ(state.is_enabled, menu_model->IsEnabledAt(index));
    EXPECT_EQ(state.is_checked, menu_model->IsItemCheckedAt(index));
  }

  void ValidateMenuState(const ui::MenuModel* menu_model,
                         const std::vector<MenuState>& states) {
    ASSERT_NE(nullptr, menu_model);
    size_t state_index = 0;
    for (size_t i = 0; i < menu_model->GetItemCount(); ++i) {
      ASSERT_LT(state_index, states.size());
      ValidateItemState(menu_model, i, states[state_index++]);
    }
    EXPECT_EQ(state_index, states.size());
  }

  FakeAppListControllerDelegate* controller() { return controller_.get(); }

  FakeAppContextMenuDelegate* menu_delegate() { return menu_delegate_.get(); }

  Profile* profile() { return profile_.get(); }

  void AddToStates(const AppServiceContextMenu& menu,
                   MenuState state,
                   std::vector<MenuState>* states) {
    // If the command is not enabled do not add it to states.
    if (!menu.IsCommandIdEnabled(state.command_id))
      return;

    states->push_back(state);
  }

  scoped_refptr<extensions::Extension> MakeApp(const std::string& app_id,
                                               bool platform_app) {
    base::FilePath path;
    base::PathService::Get(chrome::DIR_TEST_DATA, &path);
    path = path.AppendASCII("extensions").AppendASCII("manifest_tests");
    base::FilePath manifest_path =
        (platform_app) ? path.AppendASCII("init_valid_platform_app.json")
                       : path.AppendASCII("hosted_app_absolute_options.json");

    JSONFileValueDeserializer deserializer(manifest_path);
    base::Value manifest = base::Value::FromUniquePtrValue(
        deserializer.Deserialize(nullptr, nullptr));

    DCHECK(manifest.is_dict());
    std::string error;
    return extensions::Extension::Create(
        path.DirName(), extensions::mojom::ManifestLocation::kInternal,
        manifest.GetDict(), extensions::Extension::NO_FLAGS, app_id, &error);
  }

  void TestExtensionApp(const std::string& app_id,
                        bool platform_app,
                        AppListControllerDelegate::Pinnable pinnable,
                        extensions::LaunchType launch_type) {
    app_service_test_.SetUp(profile());

    scoped_refptr<extensions::Extension> store = MakeApp(app_id, platform_app);
    service_->AddExtension(store.get());
    service_->EnableExtension(app_id);

    controller_ = std::make_unique<FakeAppListControllerDelegate>();
    controller_->SetAppPinnable(app_id, pinnable);
    controller_->SetExtensionLaunchType(profile(), app_id, launch_type);

    AppServiceContextMenu menu(menu_delegate(), profile(), app_id, controller(),
                               ash::AppListItemContext::kNone);
    std::unique_ptr<ui::MenuModel> menu_model = GetMenuModel(&menu);
    ASSERT_NE(nullptr, menu_model);

    std::vector<MenuState> states;
    if (!platform_app)
      AddToStates(menu, MenuState(ash::LAUNCH_NEW), &states);

    if (pinnable != AppListControllerDelegate::NO_PIN) {
      AddToStates(
          menu,
          MenuState(ash::TOGGLE_PIN,
                    pinnable != AppListControllerDelegate::PIN_FIXED, false),
          &states);
    }
    if (!platform_app)
      AddToStates(menu, MenuState(ash::OPTIONS), &states);
    AddToStates(menu, MenuState(ash::UNINSTALL), &states);
    AddToStates(menu, MenuState(ash::SHOW_APP_INFO), &states);

    ValidateMenuState(menu_model.get(), states);
  }

  scoped_refptr<extensions::Extension> MakeChromeApp() {
    std::string err;
    base::Value::Dict value;
    value.Set("name", "Chrome App");
    value.Set("version", "0.0");
    value.SetByDottedPath("app.launch.web_url", "http://google.com");
    scoped_refptr<extensions::Extension> app = extensions::Extension::Create(
        base::FilePath(), extensions::mojom::ManifestLocation::kInternal, value,
        extensions::Extension::WAS_INSTALLED_BY_DEFAULT,
        app_constants::kChromeAppId, &err);
    EXPECT_EQ(err, "");
    return app;
  }

  void TestChromeApp() {
    app_service_test_.SetUp(profile());

    scoped_refptr<extensions::Extension> store = MakeChromeApp();
    service_->AddExtension(store.get());

    controller_ = std::make_unique<FakeAppListControllerDelegate>();
    AppServiceContextMenu menu(menu_delegate(), profile(),
                               app_constants::kChromeAppId, controller(),
                               ash::AppListItemContext::kNone);
    std::unique_ptr<ui::MenuModel> menu_model = GetMenuModel(&menu);
    ASSERT_NE(nullptr, menu_model);

    std::vector<MenuState> states;
    AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_WINDOW), &states);
    if (!profile()->IsOffTheRecord())
      AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW),
                  &states);
    AddToStates(menu, MenuState(ash::SHOW_APP_INFO), &states);
    ValidateMenuState(menu_model.get(), states);
  }

  apps::AppServiceTest& app_service_test() { return app_service_test_; }

 private:
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
  display::test::TestScreen test_screen_;
  std::unique_ptr<KeyedService> menu_manager_;
  std::unique_ptr<FakeAppListControllerDelegate> controller_;
  std::unique_ptr<FakeAppContextMenuDelegate> menu_delegate_;
  std::unique_ptr<FakeAppListModelUpdater> model_updater_;
  apps::AppServiceTest app_service_test_;
};

TEST_F(AppContextMenuTest, ExtensionApp) {
  for (extensions::LaunchType launch_type = extensions::LAUNCH_TYPE_FIRST;
       launch_type < extensions::NUM_LAUNCH_TYPES;
       launch_type = static_cast<extensions::LaunchType>(launch_type + 1)) {
    AppListControllerDelegate::Pinnable pinnable;
    for (pinnable = AppListControllerDelegate::NO_PIN;
         pinnable <= AppListControllerDelegate::PIN_FIXED;
         pinnable =
             static_cast<AppListControllerDelegate::Pinnable>(pinnable + 1)) {
      for (bool is_platform_app : {false, true}) {
        TestExtensionApp(AppListTestBase::kHostedAppId, is_platform_app,
                         pinnable, launch_type);
        TestExtensionApp(AppListTestBase::kPackagedApp1Id, is_platform_app,
                         pinnable, launch_type);
        TestExtensionApp(AppListTestBase::kPackagedApp2Id, is_platform_app,
                         pinnable, launch_type);
      }
    }
  }
}

TEST_F(AppContextMenuTest, ChromeApp) {
  TestChromeApp();
}

TEST_F(AppContextMenuTest, ChromeAppInRecentAppsList) {
  app_service_test().SetUp(profile());

  scoped_refptr<extensions::Extension> app = MakeChromeApp();
  service_->AddExtension(app.get());

  // Simulate a context menu in the recent apps row.
  AppServiceContextMenu menu(menu_delegate(), profile(),
                             app_constants::kChromeAppId, controller(),
                             ash::AppListItemContext::kRecentApps);
  std::unique_ptr<ui::MenuModel> menu_model = GetMenuModel(&menu);
  ASSERT_NE(nullptr, menu_model);

  // The usual chrome menu items appear.
  std::vector<MenuState> states;
  AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_WINDOW), &states);
  AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW),
              &states);
  AddToStates(menu, MenuState(ash::SHOW_APP_INFO), &states);
  ValidateMenuState(menu_model.get(), states);
}

TEST_F(AppContextMenuTest, NonExistingExtensionApp) {
  AppServiceContextMenu menu(menu_delegate(), profile(),
                             "some_non_existing_extension_app", controller(),
                             ash::AppListItemContext::kNone);
  std::unique_ptr<ui::MenuModel> menu_model = GetMenuModel(&menu);
  EXPECT_EQ(nullptr, menu_model);
}

TEST_F(AppContextMenuTest, ArcMenu) {
  app_service_test().SetUp(profile());
  ArcAppTest arc_test;
  arc_test.SetUp(profile());

  const auto& app_info = arc_test.fake_apps()[1];
  const std::string app_id = ArcAppTest::GetAppId(*app_info);
  controller()->SetAppPinnable(app_id, AppListControllerDelegate::PIN_EDITABLE);

  arc_test.app_instance()->SendRefreshAppList(arc_test.fake_apps());

  std::unique_ptr<FakeAppServiceAppItem> item =
      GetAppListItem(profile(), app_id);

  std::unique_ptr<ui::MenuModel> menu = GetContextMenuModel(item.get());
  ASSERT_NE(nullptr, menu);

  // Separators are not added to touchable app context menus. For touchable app
  // context menus, arc app has double separator, three more app shortcuts
  // provided by arc::FakeAppInstance and two separators between shortcuts.
  const size_t expected_items = 10;

  ASSERT_EQ(expected_items, menu->GetItemCount());
  size_t index = 0;
  ValidateItemState(menu.get(), index++, MenuState(ash::LAUNCH_NEW));
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
  ValidateItemState(menu.get(), index++, MenuState(ash::UNINSTALL));
  ValidateItemState(menu.get(), index++, MenuState(ash::SHOW_APP_INFO));

  // Test activate request.
  EXPECT_EQ(0u, arc_test.app_instance()->launch_requests().size());

  menu->ActivatedAt(0);

  // Wait for the async menu item to be executed to launch the app.
  item->WaitForLaunch();

  const std::vector<std::unique_ptr<arc::FakeAppInstance::Request>>&
      launch_requests = arc_test.app_instance()->launch_requests();
  ASSERT_EQ(1u, launch_requests.size());
  EXPECT_TRUE(launch_requests[0]->IsForApp(*app_info));

  controller()->SetAppOpen(app_id, true);
  arc_test.app_instance()->SendTaskCreated(1, *app_info, std::string());

  // It is not expected that menu model is unchanged on GetContextMenuModel.
  // ARC app menu requires model to be recalculated.
  menu = GetContextMenuModel(item.get());

  // Separators are not added to touchable app context menus except for arc app
  // shortcuts, which have double separator, three more app shortcuts provided
  // by arc::FakeAppInstance and two separators between shortcuts.
  const size_t expected_items_app_open = 9;
  ASSERT_EQ(expected_items_app_open, menu->GetItemCount());
  index = 0;
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
  ValidateItemState(menu.get(), index++, MenuState(ash::UNINSTALL));
  ValidateItemState(menu.get(), index++, MenuState(ash::SHOW_APP_INFO));

  // Test that arc app shortcuts provided by arc::FakeAppInstance have a
  // separator between each app shortcut.
  EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
  for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
    EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
              base::UTF16ToUTF8(menu->GetLabelAt(index++)));
    if (index < menu->GetItemCount())
      EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
  }

  // Test launching app shortcut item.
  EXPECT_EQ(0, arc_test.app_instance()->launch_app_shortcut_item_count());
  menu->ActivatedAt(menu->GetItemCount() - 1);
  EXPECT_EQ(1, arc_test.app_instance()->launch_app_shortcut_item_count());

  // This makes all apps non-ready.
  controller()->SetAppOpen(app_id, false);
  arc_test.app_instance()->SendTaskDestroyed(1);
  arc::ConnectionObserver<arc::mojom::AppInstance>* connection_observer =
      arc_test.arc_app_list_prefs();
  connection_observer->OnConnectionClosed();

  menu = GetContextMenuModel(item.get());

  // Separators and disabled options are not added to touchable app context
  // menus. For touchable app context menus, arc app has double separator,
  // three more app shortcuts provided by arc::FakeAppInstance and two
  // separators between shortcuts.
  const size_t expected_items_reopen = 8;
  ASSERT_EQ(expected_items_reopen, menu->GetItemCount());
  index = 0;
  ValidateItemState(menu.get(), index++, MenuState(ash::LAUNCH_NEW));
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));

  // Test that arc app shortcuts provided by arc::FakeAppInstance have a
  // separator between each app shortcut.
  EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
  for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
    EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
              base::UTF16ToUTF8(menu->GetLabelAt(index++)));
    if (index < menu->GetItemCount())
      EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
  }

  // Uninstall all apps.
  arc_test.app_instance()->SendRefreshAppList(
      std::vector<arc::mojom::AppInfoPtr>());
  controller()->SetAppOpen(app_id, false);

  // No app available case.
  menu = GetContextMenuModel(item.get());
  EXPECT_EQ(nullptr, menu);
}

TEST_F(AppContextMenuTest, ArcMenuShortcut) {
  app_service_test().SetUp(profile());
  ArcAppTest arc_test;
  arc_test.SetUp(profile());

  const arc::mojom::ShortcutInfo& shortcut_info = arc_test.fake_shortcuts()[0];
  const std::string app_id = ArcAppTest::GetAppId(shortcut_info);
  controller()->SetAppPinnable(app_id, AppListControllerDelegate::PIN_EDITABLE);

  arc_test.app_instance()->SendInstallShortcuts(arc_test.fake_shortcuts());

  std::unique_ptr<AppServiceAppItem> item = GetAppListItem(profile(), app_id);

  std::unique_ptr<ui::MenuModel> menu = GetContextMenuModel(item.get());
  ASSERT_NE(nullptr, menu);
  // Separators are not added to touchable app context menus. For touchable app
  // context menus, arc app has double separator, three more app shortcuts
  // provided by arc::FakeAppInstance and two separators between shortcuts.
  const size_t expected_items = 10;
  size_t index = 0;
  ASSERT_EQ(expected_items, menu->GetItemCount());
  ValidateItemState(menu.get(), index++, MenuState(ash::LAUNCH_NEW));
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
  ValidateItemState(menu.get(), index++, MenuState(ash::UNINSTALL));
  ValidateItemState(menu.get(), index++, MenuState(ash::SHOW_APP_INFO));
  // Test that arc app shortcuts provided by arc::FakeAppInstance have a
  // separator between each app shortcut.
  EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
  for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
    EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
              base::UTF16ToUTF8(menu->GetLabelAt(index++)));
    if (index < menu->GetItemCount())
      EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
  }

  // This makes all apps non-ready. Shortcut is still uninstall-able.
  arc::ConnectionObserver<arc::mojom::AppInstance>* connection_observer =
      arc_test.arc_app_list_prefs();
  connection_observer->OnConnectionClosed();

  menu = GetContextMenuModel(item.get());
  // Separators and disabled options are not added to touchable app context
  // menus. For touchable app context menus, arc app has double separator,
  // three more app shortcuts provided by arc::FakeAppInstance and two
  // separators between shortcuts.
  const size_t expected_items_non_ready = 9;
  ASSERT_EQ(expected_items_non_ready, menu->GetItemCount());
  index = 0;
  ValidateItemState(menu.get(), index++, MenuState(ash::LAUNCH_NEW));
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
  ValidateItemState(menu.get(), index++, MenuState(ash::UNINSTALL));

  // Test that arc app shortcuts provided by arc::FakeAppInstance have a
  // separator between each app shortcut.
  EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
  for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
    EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
              base::UTF16ToUTF8(menu->GetLabelAt(index++)));
    if (index < menu->GetItemCount())
      EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
  }
}

TEST_F(AppContextMenuTest, ArcMenuStickyItem) {
  app_service_test().SetUp(profile());
  ArcAppTest arc_test;
  arc_test.SetUp(profile());

  arc_test.app_instance()->SendRefreshAppList(arc_test.fake_apps());

  {
    // Verify menu of store
    const auto& store_info = arc_test.fake_apps()[0];
    const std::string store_id = ArcAppTest::GetAppId(*store_info);
    controller()->SetAppPinnable(store_id,
                                 AppListControllerDelegate::PIN_EDITABLE);
    std::unique_ptr<AppServiceAppItem> item =
        GetAppListItem(profile(), store_id);
    std::unique_ptr<ui::MenuModel> menu = GetContextMenuModel(item.get());
    ASSERT_NE(nullptr, menu);

    // Separators are not added to touchable app context menus. For touchable
    // app context menus, arc app has double separator, three more app shortcuts
    // provided by arc::FakeAppInstance and two separators between shortcuts.
    size_t expected_items = 9;
    ASSERT_EQ(expected_items, menu->GetItemCount());
    size_t index = 0;
    ValidateItemState(menu.get(), index++, MenuState(ash::LAUNCH_NEW));
    ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
    ValidateItemState(menu.get(), index++, MenuState(ash::SHOW_APP_INFO));

    // Test that arc app shortcuts provided by arc::FakeAppInstance have a
    // separator between each app shortcut.
    EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
    for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
      EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
                base::UTF16ToUTF8(menu->GetLabelAt(index++)));
      if (index < menu->GetItemCount())
        EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
    }
  }
}

// In suspended state app does not have launch item.
TEST_F(AppContextMenuTest, ArcMenuSuspendedItem) {
  app_service_test().SetUp(profile());
  ArcAppTest arc_test;
  arc_test.SetUp(profile());

  std::vector<arc::mojom::AppInfoPtr> apps;
  apps.emplace_back(arc_test.fake_apps()[0]->Clone())->suspended = true;
  arc_test.app_instance()->SendRefreshAppList(apps);

  const std::string app_id = ArcAppTest::GetAppId(*apps[0]);
  controller()->SetAppPinnable(app_id, AppListControllerDelegate::PIN_EDITABLE);
  std::unique_ptr<AppServiceAppItem> item = GetAppListItem(profile(), app_id);
  std::unique_ptr<ui::MenuModel> menu = GetContextMenuModel(item.get());
  ASSERT_NE(nullptr, menu);

  // Separators are not added to touchable app context menus. For touchable
  // app context menus, arc app has double separator, three more app shortcuts
  // provided by arc::FakeAppInstance and two separators between shortcuts.
  size_t expected_items = 8;
  ASSERT_EQ(expected_items, menu->GetItemCount());
  size_t index = 0;
  ValidateItemState(menu.get(), index++, MenuState(ash::TOGGLE_PIN));
  ValidateItemState(menu.get(), index++, MenuState(ash::SHOW_APP_INFO));

  // Test that arc app shortcuts provided by arc::FakeAppInstance have a
  // separator between each app shortcut.
  EXPECT_EQ(ui::DOUBLE_SEPARATOR, menu->GetSeparatorTypeAt(index++));
  for (int shortcut_index = 0; index < menu->GetItemCount(); ++index) {
    EXPECT_EQ(base::StringPrintf("ShortLabel %d", shortcut_index++),
              base::UTF16ToUTF8(menu->GetLabelAt(index++)));
    if (index < menu->GetItemCount())
      EXPECT_EQ(ui::PADDED_SEPARATOR, menu->GetSeparatorTypeAt(index));
  }
}

TEST_F(AppContextMenuTest, CommandIdsMatchEnumsForHistograms) {
  // Tests that CommandId enums are not changed as the values are used in
  // histograms.
  EXPECT_EQ(9, ash::NOTIFICATION_CONTAINER);
  EXPECT_EQ(100, ash::LAUNCH_NEW);
  EXPECT_EQ(101, ash::TOGGLE_PIN);
  EXPECT_EQ(102, ash::SHOW_APP_INFO);
  EXPECT_EQ(103, ash::OPTIONS);
  EXPECT_EQ(104, ash::UNINSTALL);
  EXPECT_EQ(105, ash::REMOVE_FROM_FOLDER);
  EXPECT_EQ(106, ash::APP_CONTEXT_MENU_NEW_WINDOW);
  EXPECT_EQ(107, ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW);
  EXPECT_EQ(108, ash::INSTALL);
  EXPECT_EQ(200, ash::USE_LAUNCH_TYPE_COMMAND_START);
  EXPECT_EQ(200, ash::DEPRECATED_USE_LAUNCH_TYPE_PINNED);
  EXPECT_EQ(201, ash::USE_LAUNCH_TYPE_REGULAR);
  EXPECT_EQ(202, ash::DEPRECATED_USE_LAUNCH_TYPE_FULLSCREEN);
  EXPECT_EQ(203, ash::USE_LAUNCH_TYPE_WINDOW);
  EXPECT_EQ(204, ash::USE_LAUNCH_TYPE_TABBED_WINDOW);
}

// Tests that internal app's context menu is correct.
TEST_F(AppContextMenuTest, InternalAppMenu) {
  for (const auto& internal_app : GetInternalAppList(profile())) {
    controller()->SetAppPinnable(internal_app.app_id,
                                 AppListControllerDelegate::PIN_EDITABLE);

    std::unique_ptr<AppServiceAppItem> item =
        GetAppListItem(profile(), internal_app.app_id);
    std::unique_ptr<ui::MenuModel> menu = GetContextMenuModel(item.get());
    ASSERT_NE(nullptr, menu);
    EXPECT_EQ(1u, menu->GetItemCount());
    ValidateItemState(menu.get(), 0, MenuState(ash::TOGGLE_PIN));
  }
}

// Lacros has its own test suite because the feature needs to be enabled before
// SetUp().
class AppContextMenuLacrosTest : public AppContextMenuTest {
 public:
  AppContextMenuLacrosTest() {
    feature_list_.InitWithFeatures(
        ash::standalone_browser::GetFeatureRefs(),
        {ash::features::kEnforceAshExtensionKeeplist});
    ash::standalone_browser::migrator_util::SetProfileMigrationCompletedForTest(
        true);
    scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
        ash::switches::kEnableLacrosForTesting);
  }
  AppContextMenuLacrosTest(const AppContextMenuLacrosTest&) = delete;
  AppContextMenuLacrosTest& operator=(const AppContextMenuLacrosTest&) = delete;
  ~AppContextMenuLacrosTest() override = default;

  // testing::Test:
  void SetUp() override {
    fake_user_manager_.Reset(std::make_unique<ash::FakeChromeUserManager>());

    // Login a user. The "email" must match the TestingProfile's
    // GetProfileUserName() so that profile() will be the primary profile.
    const AccountId account_id = AccountId::FromUserEmail("testing_profile");
    fake_user_manager_->AddUser(account_id);
    fake_user_manager_->LoginUser(account_id);

    // Creates profile().
    AppContextMenuTest::SetUp();

    ASSERT_TRUE(ash::ProfileHelper::Get()->IsPrimaryProfile(profile()));
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  base::test::ScopedCommandLine scoped_command_line_;
  user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
      fake_user_manager_;
};

TEST_F(AppContextMenuLacrosTest, LacrosApp) {
  app_service_test().SetUp(profile());

  // Create the context menu.
  AppServiceContextMenu menu(menu_delegate(), profile(),
                             app_constants::kLacrosAppId, controller(),
                             ash::AppListItemContext::kNone);
  std::unique_ptr<ui::MenuModel> menu_model = GetMenuModel(&menu);
  ASSERT_NE(menu_model, nullptr);

  // Verify expected menu items.
  // It should have, Open new window, Open incognito window, and app info.
  EXPECT_EQ(menu_model->GetItemCount(), 3u);
  std::vector<MenuState> states;
  AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_WINDOW), &states);
  AddToStates(menu, MenuState(ash::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW),
              &states);
  AddToStates(menu, MenuState(ash::SHOW_APP_INFO), &states);
  ValidateMenuState(menu_model.get(), states);
}

}  // namespace app_list