// Copyright 2024 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/mahi/mahi_manager_impl.h"
#include <memory>
#include <string>
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/toast/anchored_nudge_manager_impl.h"
#include "ash/test/ash_test_base.h"
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
#include "chrome/browser/ash/mahi/fake_mahi_browser_delegate_ash.h"
#include "chrome/browser/ash/mahi/mahi_cache_manager.h"
#include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/mahi.mojom-forward.h"
#include "chromeos/crosapi/mojom/mahi.mojom.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "content/public/test/browser_task_environment.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/lottie/resource.h"
#include "ui/views/widget/widget.h"
namespace {
using ::testing::IsNull;
constexpr char kFakeSummary[] = "Fake summary";
class FakeMahiProvider : public manta::MahiProvider {
public:
FakeMahiProvider(
scoped_refptr<network::SharedURLLoaderFactory> test_url_loader_factory,
signin::IdentityManager* identity_manager)
: MahiProvider(std::move(test_url_loader_factory), identity_manager) {}
void Summarize(const std::string& input,
const std::string& title,
const std::optional<std::string>& url,
manta::MantaGenericCallback callback) override {
++num_summarize_call_;
latest_title_ = title;
latest_url_ = url;
std::move(callback).Run(base::Value::Dict().Set("outputData", kFakeSummary),
{manta::MantaStatusCode::kOk, "Status string ok"});
}
// Counts the number of call to `Summarize()`
int NumberOfSumarizeCall() { return num_summarize_call_; }
const std::string& latest_title() const { return latest_title_; }
const std::optional<std::string>& latest_url() const { return latest_url_; }
private:
int num_summarize_call_ = 0;
std::string latest_title_;
std::optional<std::string> latest_url_;
};
bool IsMahiNudgeShown() {
return ash::Shell::Get()->anchored_nudge_manager()->IsNudgeShown(
ash::mahi_constants::kMahiNudgeId);
}
} // namespace
namespace ash {
class MahiManagerImplTest : public NoSessionAshTestBase {
public:
MahiManagerImplTest()
: NoSessionAshTestBase(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
// Sets the default functions for the test to create image with the lottie
// resource id. Otherwise there's no `g_parse_lottie_as_still_image_` set in
// the `ResourceBundle`.
ui::ResourceBundle::SetLottieParsingFunctions(
&lottie::ParseLottieAsStillImage,
&lottie::ParseLottieAsThemedStillImage);
}
MahiManagerImplTest(const MahiManagerImplTest&) = delete;
MahiManagerImplTest& operator=(const MahiManagerImplTest&) = delete;
~MahiManagerImplTest() override = default;
// NoSessionAshTestBase::
void SetUp() override {
feature_list_.InitWithFeatures(
/*enabled_features=*/{chromeos::features::kMahi,
chromeos::features::kFeatureManagementMahi},
/*disabled_features=*/{});
NoSessionAshTestBase::SetUp();
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kMahiRestrictionsOverride);
magic_boost_state_ = std::make_unique<MagicBoostStateAsh>();
mahi_manager_impl_ = std::make_unique<MahiManagerImpl>();
mahi_manager_impl_->mahi_provider_ = CreateMahiProvider();
fake_mahi_browser_delegate_ash_ =
std::make_unique<FakeMahiBrowserDelegateAsh>();
mahi_manager_impl_->mahi_browser_delegate_ash_ =
fake_mahi_browser_delegate_ash_.get();
CreateUserSessions(1);
}
void TearDown() override {
mahi_manager_impl_.reset();
magic_boost_state_.reset();
fake_mahi_browser_delegate_ash_.reset();
NoSessionAshTestBase::TearDown();
}
void SetMahiEnabledByUserPref(bool enabled) {
Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
ash::prefs::kHmrEnabled, enabled);
}
FakeMahiProvider* GetMahiProvider() {
return static_cast<FakeMahiProvider*>(
mahi_manager_impl_->mahi_provider_.get());
}
bool IsEnabled() const { return mahi_manager_impl_->IsEnabled(); }
crosapi::mojom::MahiPageInfoPtr CreatePageInfo(const std::string& url,
const std::u16string& title,
bool is_incognito = false) {
return crosapi::mojom::MahiPageInfo::New(
/*client_id=*/base::UnguessableToken(),
/*page_id=*/base::UnguessableToken(), /*url=*/GURL(url),
/*title=*/title,
/*favicon_image=*/gfx::ImageSkia(), /*is_distillable=*/true,
/*is_incognito=*/is_incognito);
}
MahiCacheManager* GetCacheManager() {
return mahi_manager_impl_->cache_manager_.get();
}
void NotifyRefreshAvailability(bool available) {
mahi_manager_impl_->NotifyRefreshAvailability(available);
}
// void RequestSummary(const std::string& url = "http://url1.com/abc#skip",
// bool incognito = false) {
void RequestSummary(bool incognito = false,
const std::string& url = "http://url1.com/abc#skip") {
// Sets the page that needed to get summary.
mahi_manager_impl_->SetCurrentFocusedPageInfo(
CreatePageInfo(url, /*title=*/u"Title of url1",
/*is_incognito=*/incognito));
// Gets the summary of the page.
mahi_manager_impl_->GetSummary(base::DoNothing());
}
protected:
std::unique_ptr<FakeMahiProvider> CreateMahiProvider() {
return std::make_unique<FakeMahiProvider>(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_),
identity_test_env_.identity_manager());
}
std::unique_ptr<MagicBoostStateAsh> magic_boost_state_;
std::unique_ptr<MahiManagerImpl> mahi_manager_impl_;
base::test::ScopedFeatureList feature_list_;
private:
network::TestURLLoaderFactory test_url_loader_factory_;
signin::IdentityTestEnvironment identity_test_env_;
std::unique_ptr<FakeMahiBrowserDelegateAsh> fake_mahi_browser_delegate_ash_;
};
// Title is included in the request proto.
TEST_F(MahiManagerImplTest, SendingTitleOnly) {
RequestSummary();
EXPECT_EQ(GetMahiProvider()->latest_title(), "Title of url1");
EXPECT_FALSE(GetMahiProvider()->latest_url().has_value());
}
// Url, on the other hand, is controlled by kMahiSendingUrl.
TEST_F(MahiManagerImplTest, SendingTitleAndUrl) {
feature_list_.Reset();
feature_list_.InitWithFeatures(
{chromeos::features::kMahi, chromeos::features::kMahiSendingUrl,
chromeos::features::kFeatureManagementMahi},
/*disabled_features=*/{});
RequestSummary();
EXPECT_TRUE(GetMahiProvider()->latest_url().has_value());
EXPECT_EQ(GetMahiProvider()->latest_url().value(),
"http://url1.com/abc#skip");
// The fake url we make up for media app pdf files is ignored.
RequestSummary(false, "file:///media-app/example.pdf");
EXPECT_FALSE(GetMahiProvider()->latest_url().has_value());
}
TEST_F(MahiManagerImplTest, CacheSavedForSummaryRequest) {
// No cache yet.
EXPECT_EQ(GetCacheManager()->size(), 0);
RequestSummary();
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
auto summary = GetCacheManager()->GetSummaryForUrl("http://url1.com/abc");
EXPECT_EQ(GetMahiProvider()->NumberOfSumarizeCall(), 1);
EXPECT_TRUE(summary.has_value());
EXPECT_EQ(base::UTF16ToUTF8(summary.value()), kFakeSummary);
}
TEST_F(MahiManagerImplTest, NoCacheSavedForIncognitoPage) {
// No cache at the beginning.
EXPECT_EQ(GetCacheManager()->size(), 0);
// Request summary from a incognito page.
RequestSummary(/*incognito=*/true);
// Summary is not saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 0);
// Request summary from a normal page.
RequestSummary(/*incognito=*/false);
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
}
TEST_F(MahiManagerImplTest, NoSummaryCallWhenSummaryIsInCache) {
// Adds some content to the cache.
const std::u16string new_summary(u"new summary");
GetCacheManager()->AddCacheForUrl(
"http://url1.com/abc#random",
MahiCacheManager::MahiData(
/*url=*/"http://url1.com/abc#skip", /*title=*/u"Title of url1",
/*page_content=*/u"Page content", /*favicon_image=*/std::nullopt,
/*summary=*/new_summary, /*previous_qa=*/{}));
RequestSummary();
auto summary = GetCacheManager()->GetSummaryForUrl("http://url1.com/abc");
// No call is made to MahiProvider.
EXPECT_EQ(GetMahiProvider()->NumberOfSumarizeCall(), 0);
EXPECT_TRUE(summary.has_value());
EXPECT_EQ(summary.value(), new_summary);
}
TEST_F(MahiManagerImplTest, ClearAllCacheWhenAllHistoryAreBeingCleared) {
// No cache yet.
EXPECT_EQ(GetCacheManager()->size(), 0);
RequestSummary();
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
mahi_manager_impl_->OnHistoryDeletions(
nullptr, history::DeletionInfo::ForAllHistory());
// Cache should be empty
EXPECT_EQ(GetCacheManager()->size(), 0);
}
TEST_F(MahiManagerImplTest, ClearURLs) {
// No cache yet.
EXPECT_EQ(GetCacheManager()->size(), 0);
RequestSummary();
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
// Try to delete URLs that aren't in the cache.
{
const auto kUrl1 = GURL("http://www.a.com");
const auto kUrl2 = GURL("http://www.b.com");
history::URLRows urls_to_delete = {history::URLRow(kUrl1),
history::URLRow(kUrl2)};
history::DeletionInfo deletion_info =
history::DeletionInfo::ForUrls(urls_to_delete, std::set<GURL>());
mahi_manager_impl_->OnHistoryDeletions(nullptr, deletion_info);
// Cache size doesn't change.
EXPECT_EQ(GetCacheManager()->size(), 1);
}
// List of URLs contains a URL that is in the cache.
{
const auto kUrl1 = GURL("http://www.a.com");
const auto kUrl2 = GURL("http://url1.com/abc#should_delete");
history::URLRows urls_to_delete = {history::URLRow(kUrl1),
history::URLRow(kUrl2)};
history::DeletionInfo deletion_info =
history::DeletionInfo::ForUrls(urls_to_delete, std::set<GURL>());
mahi_manager_impl_->OnHistoryDeletions(nullptr, deletion_info);
// The URL should be deleted from the cache.
EXPECT_EQ(GetCacheManager()->size(), 0);
}
}
TEST_F(MahiManagerImplTest, TurnOffSettingsClearCache) {
// No cache yet.
EXPECT_EQ(GetCacheManager()->size(), 0);
RequestSummary();
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
// Cache must be empty after user turn off the settings.
SetMahiEnabledByUserPref(false);
EXPECT_EQ(GetCacheManager()->size(), 0);
}
TEST_F(MahiManagerImplTest, ClearCacheSuccessfully) {
// No cache yet.
EXPECT_EQ(GetCacheManager()->size(), 0);
RequestSummary();
// Summary is saved in the cache.
EXPECT_EQ(GetCacheManager()->size(), 1);
// Cache must be empty after cleared.
mahi_manager_impl_->ClearCache();
EXPECT_EQ(GetCacheManager()->size(), 0);
}
TEST_F(MahiManagerImplTest, SetMahiPrefOnLogin) {
// Checks that it should work for both when the first user's default pref is
// true or false.
for (bool mahi_enabled : {false, true}) {
// Sets the pref for the default user.
SetMahiEnabledByUserPref(mahi_enabled);
ASSERT_EQ(IsEnabled(), mahi_enabled);
const AccountId user1_account_id =
Shell::Get()->session_controller()->GetActiveAccountId();
// Sets the pref for the second user.
SimulateUserLogin("[email protected]");
SetMahiEnabledByUserPref(!mahi_enabled);
EXPECT_EQ(IsEnabled(), !mahi_enabled);
// Switching back to the previous user will update to correct pref.
GetSessionControllerClient()->SwitchActiveUser(user1_account_id);
EXPECT_EQ(IsEnabled(), mahi_enabled);
// Clears all logins and re-logins the default user.
GetSessionControllerClient()->Reset();
SimulateUserLogin(user1_account_id);
}
}
TEST_F(MahiManagerImplTest, OnPreferenceChanged) {
for (bool mahi_enabled : {false, true, false}) {
SetMahiEnabledByUserPref(mahi_enabled);
EXPECT_EQ(IsEnabled(), mahi_enabled);
}
}
// Tests that the Mahi educational nudge is shown when the user visits eligible
// content and they have not opted in to the feature.
TEST_F(MahiManagerImplTest, ShowEducationalNudge) {
SetMahiEnabledByUserPref(false);
EXPECT_FALSE(IsMahiNudgeShown());
// Notifying that a refresh is not available should have no effect.
NotifyRefreshAvailability(/*available=*/false);
EXPECT_FALSE(IsMahiNudgeShown());
// Notifying that a refresh is available should show the nudge.
NotifyRefreshAvailability(/*available=*/true);
EXPECT_TRUE(IsMahiNudgeShown());
// Notifying that a refresh is not available should have no effect.
NotifyRefreshAvailability(/*available=*/false);
EXPECT_TRUE(IsMahiNudgeShown());
}
} // namespace ash