// Copyright 2021 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/files/file_search_provider.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/files/file_result.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chrome/browser/ash/app_list/search/test/test_search_controller.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_common_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace app_list::test {
namespace {
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::UnorderedElementsAre;
MATCHER_P(Title, title, "") {
return base::UTF16ToUTF8(arg->title()) == title;
}
} // namespace
class FileSearchProviderTest : public testing::Test,
public testing::WithParamInterface<bool> {
public:
FileSearchProviderTest() {
if (GetParam()) {
scoped_feature_list_.InitAndEnableFeature(
search_features::kLauncherFuzzyMatchAcrossProviders);
} else {
scoped_feature_list_.InitAndDisableFeature(
search_features::kLauncherFuzzyMatchAcrossProviders);
}
}
protected:
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
search_controller_ = std::make_unique<TestSearchController>();
auto provider = std::make_unique<FileSearchProvider>(
profile_.get(), base::FileEnumerator::FileType::FILES |
base::FileEnumerator::FileType::DIRECTORIES);
provider_ = provider.get();
search_controller_->AddProvider(std::move(provider));
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
provider_->SetRootPathForTesting(scoped_temp_dir_.GetPath());
Wait();
}
base::FilePath Path(const std::string& filename) {
return scoped_temp_dir_.GetPath().Append(filename);
}
void WriteFile(const std::string& filename) {
ASSERT_TRUE(base::WriteFile(Path(filename), "abcd"));
ASSERT_TRUE(base::PathExists(Path(filename)));
Wait();
}
void CreateDirectory(const std::string& directory_name) {
ASSERT_TRUE(base::CreateDirectory(Path(directory_name)));
ASSERT_TRUE(base::PathExists(Path(directory_name)));
Wait();
}
void StartSearch(const std::u16string& query) {
search_controller_->StartSearch(query);
}
const SearchProvider::Results& LastResults() {
return search_controller_->last_results();
}
void Wait() { task_environment_.RunUntilIdle(); }
content::BrowserTaskEnvironment task_environment_;
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<Profile> profile_;
std::unique_ptr<TestSearchController> search_controller_;
raw_ptr<FileSearchProvider> provider_;
base::ScopedTempDir scoped_temp_dir_;
};
INSTANTIATE_TEST_SUITE_P(FuzzyMatchForProviders,
FileSearchProviderTest,
testing::Bool());
TEST_P(FileSearchProviderTest, SearchResultsMatchQuery) {
WriteFile("file_1.txt");
WriteFile("no_match.png");
WriteFile("my_file_2.png");
StartSearch(u"file");
Wait();
EXPECT_THAT(LastResults(), UnorderedElementsAre(Title("file_1.txt"),
Title("my_file_2.png")));
}
TEST_P(FileSearchProviderTest, SearchIsCaseInsensitive) {
WriteFile("FILE_1.png");
WriteFile("FiLe_2.Png");
StartSearch(u"fIle");
Wait();
EXPECT_THAT(LastResults(),
UnorderedElementsAre(Title("FILE_1.png"), Title("FiLe_2.Png")));
}
TEST_P(FileSearchProviderTest, SearchIsAccentAndCaseInsensitive) {
WriteFile("FĪLE_1.png");
WriteFile("FīLe_2.Png");
StartSearch(u"fīle");
Wait();
EXPECT_THAT(LastResults(),
UnorderedElementsAre(Title("FĪLE_1.png"), Title("FīLe_2.Png")));
}
TEST_P(FileSearchProviderTest, SearchIsAccentInsensitive) {
WriteFile("FILE_1.png");
WriteFile("FiLe_2.Png");
WriteFile("FĪLE_3.png");
WriteFile("FīLe_4.Png");
WriteFile("FiLË_5.png");
WriteFile("FILê_6.Png");
StartSearch(u"file");
Wait();
EXPECT_THAT(LastResults(),
UnorderedElementsAre(Title("FILE_1.png"), Title("FiLe_2.Png"),
Title("FĪLE_3.png"), Title("FīLe_4.Png"),
Title("FiLË_5.png"), Title("FILê_6.Png")));
}
TEST_P(FileSearchProviderTest, SearchIsAccentHonored) {
WriteFile("FĪLE_1.png");
WriteFile("FīLe_2.Png");
WriteFile("file_3.png");
StartSearch(u"fīle");
Wait();
EXPECT_THAT(LastResults(),
UnorderedElementsAre(Title("FĪLE_1.png"), Title("FīLe_2.Png")));
}
TEST_P(FileSearchProviderTest, SearchDirectories) {
CreateDirectory("my_folder");
StartSearch(u"my_folder");
Wait();
EXPECT_THAT(LastResults(), UnorderedElementsAre(Title("my_folder")));
}
TEST_P(FileSearchProviderTest, DoesNotSearchDirectoriesIfTurnedOff) {
provider_->SetFileTypeForTesting(base::FileEnumerator::FileType::FILES);
CreateDirectory("my_folder");
StartSearch(u"my_folder");
Wait();
EXPECT_THAT(LastResults(), IsEmpty());
}
TEST_P(FileSearchProviderTest, ResultMetadataTest) {
WriteFile("file.txt");
StartSearch(u"file");
Wait();
ASSERT_TRUE(LastResults().size() == 1u);
const auto& result = LastResults()[0];
EXPECT_EQ(result->result_type(), ash::AppListSearchResultType::kFileSearch);
EXPECT_EQ(result->display_type(), ash::SearchResultDisplayType::kList);
}
TEST_P(FileSearchProviderTest, RecentlyAccessedFilesHaveHigherRelevance) {
WriteFile("file.txt");
WriteFile("file.png");
WriteFile("file.pdf");
// Set the access times of all files to be different.
const base::Time time = base::Time::Now();
const base::Time earlier_time = time - base::Days(5);
const base::Time earliest_time = time - base::Days(10);
TouchFile(Path("file.txt"), time, time);
TouchFile(Path("file.png"), earliest_time, time);
TouchFile(Path("file.pdf"), earlier_time, time);
StartSearch(u"file");
Wait();
ASSERT_TRUE(LastResults().size() == 3u);
// Sort the results by descending relevance.
std::vector<ChromeSearchResult*> results;
for (const auto& result : LastResults()) {
results.push_back(result.get());
}
std::sort(results.begin(), results.end(),
[](const ChromeSearchResult* a, const ChromeSearchResult* b) {
return a->relevance() > b->relevance();
});
ASSERT_TRUE(results[0]->relevance() > results[1]->relevance());
ASSERT_TRUE(results[1]->relevance() > results[2]->relevance());
// Most recently accessed files should be at the front.
EXPECT_THAT(results, ElementsAre(Title("file.txt"), Title("file.pdf"),
Title("file.png")));
}
TEST_P(FileSearchProviderTest, HighScoringFilesHaveScoreInRightRange) {
// Make two identically named files with different access times.
const base::Time time = base::Time::Now();
const base::Time earlier_time = time - base::Days(5);
CreateDirectory("dir");
WriteFile("dir/file");
WriteFile("file");
TouchFile(Path("dir/file"), time, time);
TouchFile(Path("file"), earlier_time, time);
// Match them perfectly, so both score 1.0.
StartSearch(u"file");
Wait();
ASSERT_EQ(LastResults().size(), 2u);
// Sort the results by descending relevance.
std::vector<ChromeSearchResult*> results;
for (const auto& result : LastResults()) {
results.push_back(result.get());
}
std::sort(results.begin(), results.end(),
[](const ChromeSearchResult* a, const ChromeSearchResult* b) {
return a->relevance() > b->relevance();
});
// The scores should be properly in order and not exceed 1.0.
EXPECT_GT(results[0]->relevance(), results[1]->relevance());
EXPECT_LE(results[0]->relevance(), 1.0);
}
TEST_P(FileSearchProviderTest, ResultsNotReturnedAfterClearingSearch) {
// Make two identically named files with different access times.
const base::Time time = base::Time::Now();
const base::Time earlier_time = time - base::Days(5);
CreateDirectory("dir");
WriteFile("file");
TouchFile(Path("file"), earlier_time, time);
// Start search, and cancel it before the provider has had a chance to return
// results.
StartSearch(u"file");
provider_->StopQuery();
Wait();
EXPECT_EQ(LastResults().size(), 0u);
}
class FileSearchProviderTrashTest : public FileSearchProviderTest {
public:
FileSearchProviderTrashTest() = default;
FileSearchProviderTrashTest(const FileSearchProviderTrashTest&) = delete;
FileSearchProviderTrashTest& operator=(const FileSearchProviderTrashTest&) =
delete;
void SetUp() override {
FileSearchProviderTest::SetUp();
// Ensure the MyFiles and Downloads mount points are appropriately mocked
// to allow the trash locations to be parented at the test directory.
storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
file_manager::util::GetDownloadsMountPointName(profile_.get()),
storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
scoped_temp_dir_.GetPath());
ToggleTrash(true);
}
void ToggleTrash(bool enabled) {
profile_->GetPrefs()->SetBoolean(ash::prefs::kFilesAppTrashEnabled,
enabled);
}
};
INSTANTIATE_TEST_SUITE_P(FuzzyMatchForProviders,
FileSearchProviderTrashTest,
testing::Values(true));
TEST_P(FileSearchProviderTrashTest, FilesInTrashAreIgnored) {
using file_manager::trash::kTrashFolderName;
CreateDirectory(kTrashFolderName);
WriteFile("file");
WriteFile(base::FilePath(kTrashFolderName).Append("trashed_file").value());
StartSearch(u"file");
Wait();
EXPECT_THAT(LastResults(), UnorderedElementsAre(Title("file")));
}
TEST_P(FileSearchProviderTrashTest, FilesInTrashArentIgnoredIfTrashDisabled) {
using file_manager::trash::kTrashFolderName;
ToggleTrash(false);
CreateDirectory(kTrashFolderName);
WriteFile("file");
WriteFile(base::FilePath(kTrashFolderName).Append("trashed_file").value());
StartSearch(u"file");
Wait();
EXPECT_THAT(LastResults(),
UnorderedElementsAre(Title("file"), Title("trashed_file")));
}
} // namespace app_list::test