// 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 "components/webapps/browser/android/webapk/webapk_proto_builder.h"
#include "base/files/file_path.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "components/webapk/webapk.pb.h"
#include "components/webapps/browser/android/shortcut_info.h"
#include "components/webapps/browser/android/webapp_icon.h"
#include "components/webapps/browser/features.h"
#include "content/public/test/browser_task_environment.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/skia/include/core/SkColor.h"
namespace {
const base::FilePath::CharType kTestDataDir[] =
FILE_PATH_LITERAL("components/test/data/webapps");
// The URLs of best icons from Web Manifest. We use a random file in the test
// data directory. Since WebApkInstaller does not try to decode the file as an
// image it is OK that the file is not an image.
const char* kBestPrimaryIconUrl = "/simple.html";
const char* kBestSplashIconUrl = "/nostore.html";
const char* kBestShortcutIconUrl = "/title1.html";
const char* kUnusedIconPath = "https://example.com/unused_icon.png";
std::unique_ptr<webapps::WebappIcon> BuildTestWebApkIcon(
GURL icon_url,
std::string data,
std::string hash,
std::set<webapk::Image::Usage> usages) {
auto icon = std::make_unique<webapps::WebappIcon>(icon_url);
icon->SetData(std::move(data));
icon->set_hash(std::move(hash));
for (const auto& usage : usages) {
icon->AddUsage(usage);
}
return icon;
}
} // namespace
// Builds WebApk proto and blocks till done.
class BuildProtoRunner {
public:
BuildProtoRunner() {}
BuildProtoRunner(const BuildProtoRunner&) = delete;
BuildProtoRunner& operator=(const BuildProtoRunner&) = delete;
~BuildProtoRunner() {}
void BuildSync(
const GURL& best_primary_icon_url,
const GURL& splash_image_url,
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons,
std::unique_ptr<webapps::WebappIcon> primary_icon,
std::unique_ptr<webapps::WebappIcon> splash_icon,
const GURL& manifest_id,
const GURL& app_key,
const std::optional<SkColor>& dark_theme_color,
const std::optional<SkColor>& dark_background_color,
bool is_manifest_stale,
bool is_app_identity_update_supported,
const std::vector<GURL>& best_shortcut_icon_urls) {
webapps::ShortcutInfo info{GURL()};
info.manifest_id = manifest_id;
info.dark_theme_color = dark_theme_color;
info.dark_background_color = dark_background_color;
info.best_primary_icon_url = best_primary_icon_url;
info.splash_image_url = splash_image_url;
if (!best_primary_icon_url.is_empty())
info.icon_urls.push_back(best_primary_icon_url.spec());
if (!splash_image_url.is_empty())
info.icon_urls.push_back(splash_image_url.spec());
info.icon_urls.push_back(kUnusedIconPath);
for (const GURL& shortcut_url : best_shortcut_icon_urls) {
info.best_shortcut_icon_urls.push_back(shortcut_url);
info.shortcut_items.emplace_back();
info.shortcut_items.back().icons.emplace_back();
info.shortcut_items.back().icons.back().src = shortcut_url;
}
webapps::BuildProto(info, app_key, std::move(primary_icon),
std::move(splash_icon), "" /* package_name */,
"" /* version */, std::move(webapk_icons),
is_manifest_stale, is_app_identity_update_supported,
base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
base::Unretained(this)));
run_loop_.Run();
}
// Helper function to build proto without |primary_icon|, similar to install
// new WebAPKs. All icon information should come from |webapk_icons|
void BuildForIconInstallTestSync(
const GURL& icon_url,
std::vector<std::string> icon_urls,
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons) {
webapps::ShortcutInfo info{GURL()};
info.best_primary_icon_url = icon_url;
info.icon_urls = std::move(icon_urls);
webapps::BuildProto(info, GURL() /* app_key */, nullptr /* primary_icon */,
nullptr /* splash_icon */, "" /* package_name */,
"" /* version */, std::move(webapk_icons),
false /* is_manifest_stale */,
false /* is_app_identity_update_supported */,
base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
base::Unretained(this)));
run_loop_.Run();
}
// Helper function to build proto with |primary_icon|, similar to update
// existing WebAPKs.
void BuildForIconUpdateTestSync(
const GURL& icon_url,
std::vector<std::string> icon_urls,
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons,
const std::string& icon_data,
const std::string& icon_hash) {
webapps::ShortcutInfo info{GURL()};
info.best_primary_icon_url = icon_url;
info.icon_urls = std::move(icon_urls);
auto primary_icon = BuildTestWebApkIcon(icon_url, icon_data, icon_hash,
{webapk::Image::PRIMARY_ICON});
webapps::BuildProto(info, GURL() /* app_key */, std::move(primary_icon),
nullptr /* splash_icon */, "" /* package_name */,
"" /* version */, std::move(webapk_icons),
false /* is_manifest_stale */,
false /* is_app_identity_update_supported */,
base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
base::Unretained(this)));
run_loop_.Run();
}
webapk::WebApk* GetWebApkRequest() { return webapk_request_.get(); }
private:
// Called when the |webapk_request_| is populated.
void OnBuiltWebApkProto(std::unique_ptr<std::string> serialized_proto) {
webapk_request_ = std::make_unique<webapk::WebApk>();
webapk_request_->ParseFromString(*serialized_proto);
run_loop_.QuitClosure().Run();
}
// The populated webapk::WebApk.
std::unique_ptr<webapk::WebApk> webapk_request_;
// Called after the |webapk_request_| is built.
base::RunLoop run_loop_;
};
class WebApkProtoBuilderTest : public ::testing::Test {
public:
std::unique_ptr<BuildProtoRunner> CreateBuildProtoRunner() {
return std::make_unique<BuildProtoRunner>();
}
WebApkProtoBuilderTest()
: task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {}
WebApkProtoBuilderTest(const WebApkProtoBuilderTest&) = delete;
WebApkProtoBuilderTest& operator=(const WebApkProtoBuilderTest&) = delete;
~WebApkProtoBuilderTest() override {}
void SetUp() override {
test_server_.AddDefaultHandlers(base::FilePath(kTestDataDir));
ASSERT_TRUE(test_server_.Start());
}
void TearDown() override { base::RunLoop().RunUntilIdle(); }
net::test_server::EmbeddedTestServer* test_server() { return &test_server_; }
private:
net::EmbeddedTestServer test_server_;
content::BrowserTaskEnvironment task_environment_;
};
// When there is no Web Manifest available for a site, an empty
// |best_primary_icon_url| and an empty |splash_image_url| is used to build a
// WebApk update request. Tests the request can be built properly.
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenManifestIsObsolete) {
GURL icon_url_1 = test_server()->GetURL("/icon1.png");
GURL icon_url_2 = test_server()->GetURL("/icon2.png");
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(icon_url_1,
BuildTestWebApkIcon(icon_url_1, "data1", "1",
{webapk::Image::PRIMARY_ICON}));
webapk_icons.emplace(icon_url_2,
BuildTestWebApkIcon(icon_url_2, "data2", "2",
{webapk::Image::SPLASH_ICON}));
auto primary_icon = BuildTestWebApkIcon(GURL(), "data3", std::string(),
{webapk::Image::PRIMARY_ICON});
auto splash_icon = BuildTestWebApkIcon(GURL(), "data4", std::string(),
{webapk::Image::SPLASH_ICON});
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(), GURL(), std::move(webapk_icons), std::move(primary_icon),
std::move(splash_icon), GURL() /*manifest_id*/, GURL() /*app_key*/,
0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
true /* is_manifest_stale */, true /* is_app_identity_update_supported */,
{});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(3, manifest.icons_size());
EXPECT_EQ("", manifest.icons(0).src());
EXPECT_FALSE(manifest.icons(0).has_hash());
EXPECT_EQ(manifest.icons(0).image_data(), "data3");
EXPECT_EQ("", manifest.icons(1).src());
EXPECT_FALSE(manifest.icons(1).has_hash());
EXPECT_EQ(manifest.icons(1).image_data(), "data4");
EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
EXPECT_FALSE(manifest.icons(2).has_hash());
EXPECT_FALSE(manifest.icons(2).has_image_data());
}
// Tests a WebApk install or update request is built properly when the Chrome
// knows the best icon URL of a site after fetching its Web Manifest.
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenManifestIsAvailable) {
GURL icon_url_1 = test_server()->GetURL("/icon.png");
GURL best_primary_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
GURL best_splash_icon_url = test_server()->GetURL(kBestSplashIconUrl);
GURL best_shortcut_icon_url = test_server()->GetURL(kBestShortcutIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(icon_url_1,
BuildTestWebApkIcon(icon_url_1, "data0", "0", {}));
webapk_icons.emplace(best_primary_icon_url,
BuildTestWebApkIcon(best_primary_icon_url, "data1", "1",
{webapk::Image::PRIMARY_ICON}));
webapk_icons.emplace(best_splash_icon_url,
BuildTestWebApkIcon(best_splash_icon_url, "data2", "2",
{webapk::Image::SPLASH_ICON}));
webapk_icons.emplace(best_shortcut_icon_url,
BuildTestWebApkIcon(best_shortcut_icon_url, "data3", "3",
{webapk::Image::SHORTCUT_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
best_primary_icon_url, best_splash_icon_url, std::move(webapk_icons),
nullptr /* primary_icon */, nullptr /*splash_icon*/,
GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {best_shortcut_icon_url});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(3, manifest.icons_size());
// Check protobuf fields for kBestPrimaryIconUrl.
EXPECT_EQ(best_primary_icon_url.spec(), manifest.icons(0).src());
EXPECT_EQ(manifest.icons(0).hash(), "1");
EXPECT_EQ(manifest.icons(0).image_data(), "data1");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
// Check protobuf fields for kBestSplashIconUrl.
EXPECT_EQ(best_splash_icon_url.spec(), manifest.icons(1).src());
EXPECT_EQ(manifest.icons(1).hash(), "2");
EXPECT_EQ(manifest.icons(1).image_data(), "data2");
EXPECT_THAT(manifest.icons(1).usages(),
testing::ElementsAre(webapk::Image::SPLASH_ICON));
// Check protobuf fields for unused icon.
EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
EXPECT_FALSE(manifest.icons(2).has_hash());
EXPECT_FALSE(manifest.icons(2).has_image_data());
// Check shortcut fields.
ASSERT_EQ(manifest.shortcuts_size(), 1);
ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).src(),
best_shortcut_icon_url.spec());
EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "3");
EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data3");
}
// Tests a WebApk install or update request is built properly when the Chrome
// knows the best icon URL of a site after fetching its Web Manifest, and
// primary icon and splash icon share the same URL.
TEST_F(WebApkProtoBuilderTest,
BuildWebApkProtoPrimaryIconAndSplashIconSameUrl) {
GURL icon_url_1 = test_server()->GetURL("/icon.png");
GURL best_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(icon_url_1,
BuildTestWebApkIcon(icon_url_1, "data1", "1", {}));
webapk_icons.emplace(best_icon_url,
BuildTestWebApkIcon(best_icon_url, "data0", "0",
{webapk::Image::PRIMARY_ICON,
webapk::Image::SPLASH_ICON,
webapk::Image::SHORTCUT_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(best_icon_url), GURL(best_icon_url), std::move(webapk_icons),
nullptr /* primary_icon */, nullptr /*splash_icon*/,
GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {GURL(best_icon_url)});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(2, manifest.icons_size());
// Check protobuf fields for icons.
EXPECT_EQ(best_icon_url, manifest.icons(0).src());
EXPECT_EQ(manifest.icons(0).hash(), "0");
EXPECT_EQ(manifest.icons(0).image_data(), "data0");
EXPECT_THAT(manifest.icons(0).usages(),
testing::UnorderedElementsAre(webapk::Image::PRIMARY_ICON,
webapk::Image::SPLASH_ICON,
webapk::Image::SHORTCUT_ICON));
// Check protobuf fields for unused icon.
EXPECT_EQ(kUnusedIconPath, manifest.icons(1).src());
EXPECT_FALSE(manifest.icons(1).has_hash());
EXPECT_FALSE(manifest.icons(1).has_image_data());
// Check shortcut fields.
ASSERT_EQ(manifest.shortcuts_size(), 1);
ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_icon_url);
EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "0");
EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data0");
}
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenWithMultipleShortcuts) {
GURL best_shortcut_icon_url1 = test_server()->GetURL(kBestShortcutIconUrl);
GURL best_shortcut_icon_url2 = test_server()->GetURL(kBestPrimaryIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(
best_shortcut_icon_url1,
BuildTestWebApkIcon(best_shortcut_icon_url1, "data1", "1",
{webapk::Image::SHORTCUT_ICON}));
webapk_icons.emplace(
best_shortcut_icon_url2,
BuildTestWebApkIcon(best_shortcut_icon_url2, "data2", "2",
{webapk::Image::SHORTCUT_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */,
{GURL(best_shortcut_icon_url1), GURL(best_shortcut_icon_url2)});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(manifest.shortcuts_size(), 2);
// Check shortcut fields.
ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_shortcut_icon_url1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "1");
EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data1");
ASSERT_EQ(manifest.shortcuts(1).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(1).icons(0).src(), best_shortcut_icon_url2);
EXPECT_EQ(manifest.shortcuts(1).icons(0).hash(), "2");
EXPECT_EQ(manifest.shortcuts(1).icons(0).image_data(), "data2");
}
TEST_F(WebApkProtoBuilderTest,
BuildWebApkProtoWhenWithMultipleShortcutsAndSameIcons) {
GURL best_shortcut_icon_url = test_server()->GetURL(kBestShortcutIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(best_shortcut_icon_url,
BuildTestWebApkIcon(best_shortcut_icon_url, "data1", "1",
{webapk::Image::SHORTCUT_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */,
{GURL(best_shortcut_icon_url), GURL(best_shortcut_icon_url)});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(manifest.shortcuts_size(), 2);
// Check shortcut fields.
ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_shortcut_icon_url);
EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "1");
EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data1");
ASSERT_EQ(manifest.shortcuts(1).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(1).icons(0).src(), best_shortcut_icon_url);
EXPECT_EQ(manifest.shortcuts(1).icons(0).hash(), "1");
// This is a duplicate icon, so the data won't be included again.
EXPECT_EQ(manifest.shortcuts(1).icons(0).image_data(), "");
}
TEST_F(WebApkProtoBuilderTest,
BuildWebApkProtoSplashIconAndShortcutIconSameUrl) {
GURL icon_url_1 = test_server()->GetURL("/icon.png");
GURL best_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(icon_url_1,
BuildTestWebApkIcon(icon_url_1, "data1", "1",
{webapk::Image::PRIMARY_ICON}));
webapk_icons.emplace(best_icon_url,
BuildTestWebApkIcon(best_icon_url, "data0", "0",
{webapk::Image::SPLASH_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(icon_url_1), GURL(best_icon_url), std::move(webapk_icons),
nullptr /* primary_icon */, nullptr /*splash_icon*/,
GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
true /* is_app_identity_update_supported */, {GURL(best_icon_url)});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(3, manifest.icons_size());
ASSERT_EQ(manifest.shortcuts_size(), 1);
// Check primary icon fields.
EXPECT_EQ(icon_url_1, manifest.icons(0).src());
EXPECT_EQ(manifest.icons(0).hash(), "1");
EXPECT_EQ(manifest.icons(0).image_data(), "data1");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
// Check splash icon fields
EXPECT_EQ(best_icon_url, manifest.icons(1).src());
EXPECT_EQ(manifest.icons(1).hash(), "0");
EXPECT_EQ(manifest.icons(1).image_data(), "data0");
EXPECT_THAT(manifest.icons(1).usages(),
testing::ElementsAre(webapk::Image::SPLASH_ICON));
// Check protobuf fields for unused icon.
EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
EXPECT_FALSE(manifest.icons(2).has_hash());
EXPECT_FALSE(manifest.icons(2).has_image_data());
// Check shortcut fields.
ASSERT_EQ(manifest.shortcuts_size(), 1);
ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_icon_url);
EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "0");
EXPECT_TRUE(manifest.shortcuts(0).icons(0).has_image_data());
EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data0");
}
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoManifestIdAndKey) {
GURL manifest_id_1 = test_server()->GetURL("/test_id");
GURL app_key_1 = test_server()->GetURL("/test_key");
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(GURL(), GURL(), std::move(webapk_icons),
nullptr /* primary_icon */, nullptr /*splash_icon*/,
manifest_id_1, app_key_1, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/,
false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
EXPECT_EQ(webapk_request->app_key(), app_key_1.spec());
EXPECT_EQ(webapk_request->manifest().id(), manifest_id_1.spec());
}
TEST_F(WebApkProtoBuilderTest, MapContainsEmptyIconUrl) {
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(GURL(""),
BuildTestWebApkIcon(GURL(""), "data", "0", {}));
auto primary_icon =
BuildTestWebApkIcon(GURL(), "primary_icon_data", std::string(),
{webapk::Image::PRIMARY_ICON});
auto splash_icon = BuildTestWebApkIcon(
GURL(), "splash_icon_data", std::string(), {webapk::Image::SPLASH_ICON});
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL() /* primary_icon_url */, GURL() /*splash_icon_url*/,
std::move(webapk_icons), std::move(primary_icon), std::move(splash_icon),
GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(3, manifest.icons_size());
// Checks the request contains primary icon and splash icon with no url and
// hash.
EXPECT_EQ(manifest.icons(0).src(), "");
EXPECT_FALSE(manifest.icons(0).has_hash());
EXPECT_EQ(manifest.icons(0).image_data(), "primary_icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
EXPECT_EQ(manifest.icons(1).src(), "");
EXPECT_FALSE(manifest.icons(0).has_hash());
EXPECT_EQ(manifest.icons(1).image_data(), "splash_icon_data");
EXPECT_THAT(manifest.icons(1).usages(),
testing::ElementsAre(webapk::Image::SPLASH_ICON));
}
TEST_F(WebApkProtoBuilderTest, EmptyPrimaryIconUrlValidSplashIcon) {
GURL splash_icon_url = test_server()->GetURL(kBestSplashIconUrl);
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(splash_icon_url,
BuildTestWebApkIcon(splash_icon_url, "data2", "2",
{webapk::Image::SPLASH_ICON}));
auto primary_icon =
BuildTestWebApkIcon(GURL(), "primary_icon_data", std::string(),
{webapk::Image::PRIMARY_ICON});
auto splash_icon =
BuildTestWebApkIcon(splash_icon_url, "splash_icon_data",
"splash_icon_hash", {webapk::Image::SPLASH_ICON});
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(), GURL(splash_icon_url), std::move(webapk_icons),
std::move(primary_icon), std::move(splash_icon), GURL() /*manifest_id*/,
GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(3, manifest.icons_size());
// Check protobuf fields for icons.
EXPECT_EQ(manifest.icons(0).src(), "");
EXPECT_FALSE(manifest.icons(0).has_hash());
EXPECT_EQ(manifest.icons(0).image_data(), "primary_icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
EXPECT_EQ(manifest.icons(1).src(), splash_icon_url);
EXPECT_EQ(manifest.icons(1).hash(), "splash_icon_hash");
EXPECT_EQ(manifest.icons(1).image_data(), "splash_icon_data");
EXPECT_THAT(manifest.icons(1).usages(),
testing::ElementsAre(webapk::Image::SPLASH_ICON));
}
TEST_F(WebApkProtoBuilderTest, IconWithoutUrl_Install) {
// Test primary icon with empty URL.
std::vector<std::string> icon_urls = {kUnusedIconPath};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(GURL(), BuildTestWebApkIcon(GURL(), "data", "hash", {}));
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "2", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconInstallTestSync(GURL(), icon_urls,
std::move(webapk_icons));
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// The dummy unused icon has src and hash but no data
EXPECT_TRUE(manifest.icons(0).has_src());
EXPECT_TRUE(manifest.icons(0).has_hash());
}
TEST_F(WebApkProtoBuilderTest, IconWithoutUrl_Update) {
// Test primary icon with empty URL.
std::vector<std::string> icon_urls = {kUnusedIconPath};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(GURL(), BuildTestWebApkIcon(GURL(), "data", "hash", {}));
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "2", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconUpdateTestSync(GURL(), icon_urls, std::move(webapk_icons),
"icon_data", "icon_hash");
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(2, manifest.icons_size());
// Icon has data and hash but no src.
EXPECT_FALSE(manifest.icons(0).has_src());
EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
// The dummy unused icon has src and hash but no data
EXPECT_TRUE(manifest.icons(1).has_src());
EXPECT_TRUE(manifest.icons(1).has_hash());
}
TEST_F(WebApkProtoBuilderTest, IconUrlNotInListAndNotInHash_Install) {
// Test primary icon URL is NOT in list and NOT in hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {kUnusedIconPath};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data1", "1", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
std::move(webapk_icons));
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// The dummy unused icon has src and hash but no data
EXPECT_TRUE(manifest.icons(0).has_src());
EXPECT_TRUE(manifest.icons(0).has_hash());
}
TEST_F(WebApkProtoBuilderTest, IconUrlNotInListAndNotInHash_Update) {
// Test primary icon has URL but NOT in list and NOT in hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {kUnusedIconPath};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data1", "1", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
std::move(webapk_icons), "icon_data",
"icon_hash");
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(2, manifest.icons_size());
// Icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
// The dummy unused icon has src and hash but no data
EXPECT_TRUE(manifest.icons(1).has_src());
EXPECT_TRUE(manifest.icons(1).has_hash());
}
TEST_F(WebApkProtoBuilderTest, IconUrlInListNotInHash_Install) {
// Test primary icon has URL in the urls list but NOT in hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {test_icon_url.spec()};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
std::move(webapk_icons));
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
// No icon added because either primary icon or list icons in hash.
ASSERT_EQ(0, manifest.icons_size());
}
TEST_F(WebApkProtoBuilderTest, IconUrlInListNotInHash_Update) {
// Test primary icon has URL in the urls list but NOT in hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {test_icon_url.spec()};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
std::move(webapk_icons), "icon_data",
"icon_hash");
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// Icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}
TEST_F(WebApkProtoBuilderTest, IconUrlNotInListInHash_Install) {
// Test primary icon has URL NOT in the urls list but in the hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(test_icon_url,
BuildTestWebApkIcon(test_icon_url, "data3", "hash3",
{webapk::Image::PRIMARY_ICON}));
webapk_icons.emplace(
GURL(kUnusedIconPath),
BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
std::move(webapk_icons));
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// Primary icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "hash3");
EXPECT_EQ(manifest.icons(0).image_data(), "data3");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}
TEST_F(WebApkProtoBuilderTest, IconUrlNotInListInHash_Update) {
// Test primary icon has URL NOT in the urls list but in the hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(test_icon_url,
BuildTestWebApkIcon(test_icon_url, "data3", "hash3",
{webapk::Image::PRIMARY_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
std::move(webapk_icons), "icon_data",
"icon_hash");
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// Icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}
TEST_F(WebApkProtoBuilderTest, IconUrlInListAndHash_Install) {
// Test primary icon has URL in list and hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {test_icon_url.spec()};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(test_icon_url,
BuildTestWebApkIcon(test_icon_url, "data4", "hash4",
{webapk::Image::PRIMARY_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
std::move(webapk_icons));
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// Icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "hash4");
EXPECT_EQ(manifest.icons(0).image_data(), "data4");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}
TEST_F(WebApkProtoBuilderTest, IconUrlInListAndHash_Update) {
// Test primary icon has URL in list and hashmap
GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
std::vector<std::string> icon_urls = {test_icon_url.spec()};
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
webapk_icons.emplace(test_icon_url,
BuildTestWebApkIcon(test_icon_url, "data4", "hash4",
{webapk::Image::PRIMARY_ICON}));
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
std::move(webapk_icons), "icon_data",
"icon_hash");
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
ASSERT_EQ(1, manifest.icons_size());
// Icon has data, src and hash
EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
EXPECT_THAT(manifest.icons(0).usages(),
testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoDarkThemeAndBackground) {
std::optional<SkColor> dark_theme_color = 0x000000;
std::optional<SkColor> dark_background_color = 0x888888;
std::string dark_theme_color_expected = "rgba(0,0,0,0)";
std::string dark_background_color_expected = "rgba(136,136,136,0)";
std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
runner->BuildSync(
GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
dark_theme_color, dark_background_color, false /* is_manifest_stale*/,
false /* is_app_identity_update_supported */, {});
webapk::WebApk* webapk_request = runner->GetWebApkRequest();
ASSERT_NE(nullptr, webapk_request);
webapk::WebAppManifest manifest = webapk_request->manifest();
EXPECT_EQ(manifest.dark_theme_color(), dark_theme_color_expected);
EXPECT_EQ(manifest.dark_background_color(), dark_background_color_expected);
}