chromium/chrome/browser/ash/app_list/search/games/game_result_unittest.cc

// Copyright 2022 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/ash/app_list/search/games/game_result.h"

#include <optional>

#include "chrome/browser/apps/app_discovery_service/app_discovery_service.h"
#include "chrome/browser/apps/app_discovery_service/game_extras.h"
#include "chrome/browser/apps/app_discovery_service/result.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/app_list/search/common/search_result_util.h"
#include "chrome/browser/ash/app_list/test/test_app_list_controller_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/skia_util.h"

namespace app_list::test {
namespace {

using ::testing::_;
using ::testing::Return;

class MockAppListControllerDelegate
    : public ::test::TestAppListControllerDelegate {
 public:
  MOCK_METHOD((std::vector<std::string>),
              GetAppIdsForUrl,
              (Profile * profile,
               const GURL& url,
               bool exclude_browsers,
               bool exclude_browser_tab_apps),
              (override));
  MOCK_METHOD(void,
              LaunchAppWithUrl,
              (Profile * profile,
               const std::string& app_id,
               int32_t event_flags,
               const GURL& url,
               apps::LaunchSource launch_source),
              (override));
};

// Creates a 50x50 yellow test icon.
gfx::ImageSkia GetTestIcon() {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(50, 50);
  bitmap.eraseColor(SK_ColorYELLOW);
  return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
}

// Creates the resized non-maskable variant of the test icon, which is a 22x22
// yellow square inside a 32x32 circle.
gfx::ImageSkia GetExpectedNonMaskableIcon() {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(22, 22);
  bitmap.eraseColor(SK_ColorYELLOW);
  return gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
      /*radius=*/16, SK_ColorWHITE, gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
}

// A mock app discovery service that can produce fake icons.
class TestAppDiscoveryService : public apps::AppDiscoveryService {
 public:
  explicit TestAppDiscoveryService(Profile* profile)
      : apps::AppDiscoveryService(profile) {}
  ~TestAppDiscoveryService() override = default;
  TestAppDiscoveryService(const TestAppDiscoveryService&) = delete;
  TestAppDiscoveryService& operator=(const TestAppDiscoveryService&) = delete;

  void GetIcon(const std::string& app_id,
               int32_t size_hint_in_dip,
               apps::ResultType result_type,
               apps::GetIconCallback callback) override {
    if (icons_available_) {
      std::move(callback).Run(GetTestIcon(), apps::DiscoveryError::kSuccess);
    } else {
      std::move(callback).Run(gfx::ImageSkia(),
                              apps::DiscoveryError::kErrorRequestFailed);
    }
  }

  void set_icons_available(bool icons_available) {
    icons_available_ = icons_available;
  }

 private:
  bool icons_available_ = true;
};

apps::Result MakeAppsResult(bool masking_allowed) {
  return apps::Result(apps::AppSource::kGames, "12345", u"Title",
                      std::make_unique<apps::GameExtras>(
                          u"SourceName", base::FilePath("/icons/test.png"),
                          masking_allowed, GURL("https://game.com/game")));
}

}  // namespace

class GameResultTest : public testing::Test {
 public:
  GameResultTest() {
    profile_ = std::make_unique<TestingProfile>();
    app_discovery_service_ =
        std::make_unique<TestAppDiscoveryService>(profile_.get());
  }

  ~GameResultTest() override = default;

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<Profile> profile_;
  ::test::TestAppListControllerDelegate list_controller_;
  MockAppListControllerDelegate mock_list_controller_;
  std::unique_ptr<TestAppDiscoveryService> app_discovery_service_;
};

TEST_F(GameResultTest, Basic) {
  apps::Result apps_result = MakeAppsResult(/*masking_allowed=*/false);
  GameResult result(profile_.get(), &list_controller_,
                    app_discovery_service_.get(), apps_result, 0.6,
                    u"SomeGame");

  EXPECT_EQ(result.title(), u"Title");
  EXPECT_EQ(StringFromTextVector(result.details_text_vector()), u"SourceName");
  EXPECT_EQ(result.accessible_name(), u"Title, SourceName");
}

TEST_F(GameResultTest, Icons) {
  // The maskable icon should not be modified from its original form.
  apps::Result maskable_app = MakeAppsResult(/*masking_allowed=*/true);
  GameResult maskable_result(profile_.get(), &list_controller_,
                             app_discovery_service_.get(), maskable_app, 0.6,
                             u"SomeGame");

  EXPECT_EQ(maskable_result.icon().dimension, kAppIconDimension);
  EXPECT_EQ(maskable_result.icon().shape, ash::SearchResultIconShape::kCircle);
  EXPECT_TRUE(gfx::BitmapsAreEqual(
      *maskable_result.icon().icon.Rasterize(nullptr).bitmap(),
      *GetTestIcon().bitmap()));

  // The non-maskable icon must be resized and placed on a white circle.
  apps::Result non_maskable_app = MakeAppsResult(/*masking_allowed=*/false);
  GameResult non_maskable_result(profile_.get(), &list_controller_,
                                 app_discovery_service_.get(), non_maskable_app,
                                 0.6, u"SomeGame");

  EXPECT_EQ(non_maskable_result.icon().dimension, kAppIconDimension);
  EXPECT_EQ(non_maskable_result.icon().shape,
            ash::SearchResultIconShape::kCircle);
  EXPECT_TRUE(gfx::BitmapsAreEqual(
      *non_maskable_result.icon().icon.Rasterize(nullptr).bitmap(),
      *GetExpectedNonMaskableIcon().bitmap()));

  // If there is no icon, then the result should be filtered out.
  app_discovery_service_->set_icons_available(false);
  apps::Result no_icon_app = MakeAppsResult(/*masking_allowed=*/false);
  GameResult no_icon_result(profile_.get(), &list_controller_,
                            app_discovery_service_.get(), no_icon_app, 0.6,
                            u"SomeGame");

  EXPECT_TRUE(no_icon_result.scoring().filtered());
}

TEST_F(GameResultTest, OpensDeepLinkURLWhenAppNotFound) {
  apps::Result apps_result = MakeAppsResult(/*masking_allowed=*/false);
  GameResult result(profile_.get(), &mock_list_controller_,
                    app_discovery_service_.get(), apps_result, 0.6,
                    u"SomeGame");

  EXPECT_CALL(mock_list_controller_,
              GetAppIdsForUrl(_, GURL("https://game.com/game"), _, _))
      .WillOnce(Return(std::vector<std::string>{}));
  EXPECT_CALL(mock_list_controller_, LaunchAppWithUrl(_, _, _, _, _)).Times(0);

  result.Open(/*event_flags*/ 0);

  EXPECT_EQ(mock_list_controller_.last_opened_url(),
            GURL("https://game.com/game"));
}

TEST_F(GameResultTest, OpensDeepLinkURLWhenAppFoundButNotAllowed) {
  apps::Result apps_result = MakeAppsResult(/*masking_allowed=*/false);
  GameResult result(profile_.get(), &mock_list_controller_,
                    app_discovery_service_.get(), apps_result, 0.6,
                    u"SomeGame");

  std::string not_allowed_app_id{"not_allowed_app"};
  EXPECT_CALL(mock_list_controller_,
              GetAppIdsForUrl(_, GURL("https://game.com/game"), _, _))
      .WillOnce(Return(std::vector<std::string>{not_allowed_app_id}));
  EXPECT_CALL(mock_list_controller_, LaunchAppWithUrl(_, _, _, _, _)).Times(0);

  result.Open(/*event_flags*/ 0);

  EXPECT_EQ(mock_list_controller_.last_opened_url(),
            GURL("https://game.com/game"));
}

TEST_F(GameResultTest, LaunchesAppWhenAppFoundAndAllowed) {
  apps::Result apps_result = MakeAppsResult(/*masking_allowed=*/false);
  GameResult result(profile_.get(), &mock_list_controller_,
                    app_discovery_service_.get(), apps_result, 0.6,
                    u"SomeGame");

  std::string found_app_id{"egmafekfmcnknbdlbfbhafbllplmjlhn"};
  EXPECT_CALL(mock_list_controller_,
              GetAppIdsForUrl(_, GURL("https://game.com/game"), _, _))
      .WillOnce(Return(std::vector<std::string>{found_app_id}));
  EXPECT_CALL(
      mock_list_controller_,
      LaunchAppWithUrl(_, found_app_id, _, GURL("https://game.com/game"),
                       apps::LaunchSource::kFromAppListQuery))
      .Times(1);

  result.Open(/*event_flags*/ 0);

  EXPECT_EQ(mock_list_controller_.last_opened_url(), GURL(""));
}
}  // namespace app_list::test