blob: e2165edfb84c2998026006e428babb791d3f35e1 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 <vector>
#include "base/command_line.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "chrome/browser/content_index/content_index_provider_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/offline_items_collection/core/offline_content_provider.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/common/content_switches.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"
using offline_items_collection::ContentId;
using offline_items_collection::OfflineContentProvider;
using offline_items_collection::OfflineItem;
using offline_items_collection::OfflineItemVisuals;
namespace {
std::string GetDescriptionIdFromOfflineItemKey(const std::string& id) {
std::string description_id;
bool result =
re2::RE2::FullMatch(id, re2::RE2("\\d+#[^#]+#(.*)"), &description_id);
EXPECT_TRUE(result);
return description_id;
}
class ContentIndexTest : public InProcessBrowserTest,
public OfflineContentProvider::Observer,
public TabStripModelObserver {
public:
ContentIndexTest() = default;
~ContentIndexTest() override = default;
void SetUpOnMainThread() override {
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->ServeFilesFromSourceDirectory("chrome/test/data");
ASSERT_TRUE(https_server_->Start());
ui_test_utils::NavigateToURL(
browser(), https_server_->GetURL("/content_index/content_index.html"));
RunScript("RegisterServiceWorker()");
auto* provider = browser()->profile()->GetContentIndexProvider();
DCHECK(provider);
provider_ = static_cast<ContentIndexProviderImpl*>(provider);
provider_->AddObserver(this);
browser()->tab_strip_model()->AddObserver(this);
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
// Runs |script| and expects it to complete successfully. |script| must
// result in a Promise. Returns the resolved contents of the Promise.
std::string RunScript(const std::string& script) {
std::string result;
RunScript(script, &result);
return result.substr(5); // Ignore the trailing `ok - `.
}
// OfflineContentProvider::Observer implementation:
void OnItemsAdded(const std::vector<OfflineItem>& items) override {
ASSERT_EQ(items.size(), 1u);
offline_items_[GetDescriptionIdFromOfflineItemKey(items[0].id.id)] =
items[0];
}
void OnItemRemoved(const ContentId& id) override {
offline_items_.erase(GetDescriptionIdFromOfflineItemKey(id.id));
}
void OnItemUpdated(
const OfflineItem& item,
const base::Optional<offline_items_collection::UpdateDelta>& update_delta)
override {
NOTREACHED();
}
// TabStripModelObserver implementation:
void TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) override {
if (wait_for_tab_change_)
std::move(wait_for_tab_change_).Run();
}
void SetTabChangeQuitClosure(base::OnceClosure closure) {
wait_for_tab_change_ = std::move(closure);
}
base::Optional<OfflineItem> GetItem(const ContentId& id) {
base::Optional<OfflineItem> out_item;
base::RunLoop run_loop;
provider_->GetItemById(id,
base::BindLambdaForTesting(
[&](const base::Optional<OfflineItem>& item) {
out_item = item;
run_loop.Quit();
}));
run_loop.Run();
return out_item;
}
std::vector<OfflineItem> GetAllItems() {
std::vector<OfflineItem> out_items;
base::RunLoop run_loop;
provider_->GetAllItems(
base::BindLambdaForTesting([&](const std::vector<OfflineItem>& items) {
out_items = items;
run_loop.Quit();
}));
run_loop.Run();
return out_items;
}
std::map<std::string, OfflineItem>& offline_items() { return offline_items_; }
ContentIndexProviderImpl* provider() { return provider_; }
private:
void RunScript(const std::string& script, std::string* result) {
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame(),
"WrapFunction(async () => " + script + ")", result));
ASSERT_TRUE(
base::StartsWith(*result, "ok - ", base::CompareCase::SENSITIVE))
<< "Unexpected result: " << *result;
}
std::map<std::string, OfflineItem> offline_items_;
ContentIndexProviderImpl* provider_;
std::unique_ptr<net::EmbeddedTestServer> https_server_;
base::OnceClosure wait_for_tab_change_;
};
IN_PROC_BROWSER_TEST_F(ContentIndexTest, OfflineItemObserversReceiveEvents) {
RunScript("AddContent('my-id-1')");
RunScript("AddContent('my-id-2')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
EXPECT_EQ(offline_items().size(), 2u);
ASSERT_TRUE(offline_items().count("my-id-1"));
EXPECT_TRUE(offline_items().count("my-id-2"));
EXPECT_EQ(RunScript("GetIds()"), "my-id-1,my-id-2");
EXPECT_EQ(GetAllItems().size(), 2u);
std::string description1 = offline_items().at("my-id-1").description;
RunScript("AddContent('my-id-1')"); // update
RunScript("DeleteContent('my-id-2')"); // delete
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
EXPECT_EQ(offline_items().size(), 1u);
ASSERT_TRUE(offline_items().count("my-id-1"));
EXPECT_FALSE(offline_items().count("my-id-2"));
// Expect the description to have been updated.
EXPECT_NE(description1, offline_items().at("my-id-1").description);
}
IN_PROC_BROWSER_TEST_F(ContentIndexTest, ContextAPI) {
EXPECT_TRUE(GetAllItems().empty());
RunScript("AddContent('my-id')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
{
const auto& observed_item = offline_items().at("my-id");
auto items = GetAllItems();
ASSERT_EQ(items.size(), 1u);
EXPECT_EQ(observed_item, items[0]);
auto item = GetItem(observed_item.id);
ASSERT_TRUE(item);
EXPECT_EQ(*item, observed_item);
}
// Overwrite the existing entry.
RunScript("AddContent('my-id')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
EXPECT_EQ(GetAllItems().size(), 1u);
// Delete the registration.
{
ContentId id = offline_items().at("my-id").id;
RunScript("DeleteContent('my-id')");
EXPECT_TRUE(GetAllItems().empty());
EXPECT_FALSE(GetItem(id));
}
}
IN_PROC_BROWSER_TEST_F(ContentIndexTest, GetVisuals) {
provider()->SetIconSizesForTesting({{92, 92}});
RunScript("AddContent('my-id')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
base::RunLoop run_loop;
SkBitmap icon;
provider()->GetVisualsForItem(
offline_items().at("my-id").id,
OfflineContentProvider::GetVisualsOptions(),
base::BindLambdaForTesting(
[&](const ContentId& id,
std::unique_ptr<OfflineItemVisuals> visuals) {
ASSERT_EQ(offline_items().at("my-id").id, id);
ASSERT_TRUE(visuals);
icon = visuals->icon.AsBitmap();
run_loop.Quit();
}));
run_loop.Run();
EXPECT_FALSE(icon.isNull());
}
IN_PROC_BROWSER_TEST_F(ContentIndexTest, LaunchUrl) {
RunScript("AddContent('my-id')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
EXPECT_EQ(browser()->tab_strip_model()->count(), 1);
GURL current_url =
browser()->tab_strip_model()->GetActiveWebContents()->GetURL();
EXPECT_TRUE(base::EndsWith(current_url.spec(),
"/content_index/content_index.html",
base::CompareCase::SENSITIVE));
provider()->OpenItem(
offline_items_collection::OpenParams(
offline_items_collection::LaunchLocation::DOWNLOAD_HOME),
offline_items().at("my-id").id);
// Wait for the page to open.
base::RunLoop run_loop;
SetTabChangeQuitClosure(run_loop.QuitClosure());
run_loop.Run();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(browser()->tab_strip_model()->count(), 2);
current_url = browser()->tab_strip_model()->GetActiveWebContents()->GetURL();
EXPECT_TRUE(base::EndsWith(current_url.spec(),
"/content_index/content_index.html?launch",
base::CompareCase::SENSITIVE));
}
IN_PROC_BROWSER_TEST_F(ContentIndexTest, UserDeletedEntryDispatchesEvent) {
RunScript("AddContent('my-id')");
base::RunLoop().RunUntilIdle(); // Wait for the provider to get the content.
provider()->RemoveItem(offline_items().at("my-id").id);
EXPECT_EQ(RunScript("waitForMessageFromServiceWorker()"), "my-id");
EXPECT_TRUE(GetAllItems().empty());
}
IN_PROC_BROWSER_TEST_F(ContentIndexTest, MetricsCollected) {
// Inititally there is no content.
{
base::HistogramTester histogram_tester;
EXPECT_TRUE(GetAllItems().empty());
histogram_tester.ExpectUniqueSample("ContentIndex.NumEntriesAvailable", 0,
1);
}
// Record that two articles were added.
{
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder ukm_recorder;
{
base::RunLoop run_loop;
ukm_recorder.SetOnAddEntryCallback(
ukm::builders::ContentIndex_Added::kEntryName,
run_loop.QuitClosure());
RunScript("AddContent('my-id-1')");
run_loop.Run();
}
{
base::RunLoop run_loop;
ukm_recorder.SetOnAddEntryCallback(
ukm::builders::ContentIndex_Added::kEntryName,
run_loop.QuitClosure());
RunScript("AddContent('my-id-2')");
run_loop.Run();
}
histogram_tester.ExpectBucketCount(
"ContentIndex.ContentAdded", blink::mojom::ContentCategory::ARTICLE, 2);
EXPECT_EQ(
ukm_recorder
.GetEntriesByName(ukm::builders::ContentIndex_Added::kEntryName)
.size(),
2u);
}
// Querying the items should record that there are 2 entries available.
{
base::HistogramTester histogram_tester;
EXPECT_EQ(GetAllItems().size(), 2u);
histogram_tester.ExpectUniqueSample("ContentIndex.NumEntriesAvailable", 2,
1);
}
// User deletion will dispatch an event.
{
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::RunLoop run_loop;
ukm_recorder.SetOnAddEntryCallback(
ukm::builders::ContentIndex_DeletedByUser::kEntryName,
run_loop.QuitClosure());
provider()->RemoveItem(offline_items().at("my-id-1").id);
EXPECT_EQ(RunScript("waitForMessageFromServiceWorker()"), "my-id-1");
run_loop.Run();
histogram_tester.ExpectBucketCount("ContentIndex.ContentDeleteEvent.Find",
blink::ServiceWorkerStatusCode::kOk, 1);
histogram_tester.ExpectBucketCount("ContentIndex.ContentDeleteEvent.Start",
blink::ServiceWorkerStatusCode::kOk, 1);
histogram_tester.ExpectBucketCount(
"ContentIndex.ContentDeleteEvent.Dispatch",
blink::ServiceWorkerStatusCode::kOk, 1);
EXPECT_EQ(ukm_recorder
.GetEntriesByName(
ukm::builders::ContentIndex_DeletedByUser::kEntryName)
.size(),
1u);
}
// Opening an article is recorded.
{
base::HistogramTester histogram_tester;
ukm::TestAutoSetUkmRecorder ukm_recorder;
provider()->OpenItem(
offline_items_collection::OpenParams(
offline_items_collection::LaunchLocation::DOWNLOAD_HOME),
offline_items().at("my-id-2").id);
// Wait for the page to open.
base::RunLoop run_loop;
SetTabChangeQuitClosure(run_loop.QuitClosure());
run_loop.Run();
histogram_tester.ExpectBucketCount("ContentIndex.ContentOpened",
blink::mojom::ContentCategory::ARTICLE,
1);
EXPECT_EQ(
ukm_recorder
.GetEntriesByName(ukm::builders::ContentIndex_Opened::kEntryName)
.size(),
1u);
}
}
} // namespace