Commit 1d535191 authored by Jay Harris's avatar Jay Harris Committed by Commit Bot

Adds support for badging scopes.

There has been significant discussion on the explainer repo
about the best way for badging to work. The API has undergone
a major redesign.

Previously, badges were associated with a specific installed
web application, which severely limited the usefulness of the
API.

In the revised API, a badge is associated with a scope. This
reflects the way that web applications work, so it is possible
to badge multiple nested applications. In addition, this
should allow us to expose the API from the service worker in
future (which is not implicitly associated with any
application). The scopes solution has the added advantage
that other, non-app contexts (such as bookmarks) could be
badged in the future.

Explainer:
https://github.com/WICG/badging/blob/master/explainer.md

Bug: 987514
Change-Id: Ib11500e1b4f26eabe402153d27176e786c33768a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1723849
Commit-Queue: Matt Giuca <mgiuca@chromium.org>
Reviewed-by: default avatarMatt Giuca <mgiuca@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#692412}
parent 4805b398
......@@ -2994,6 +2994,7 @@ jumbo_split_static_library("browser") {
"background/background_contents_service_observer.h",
"badging/badge_manager.cc",
"badging/badge_manager.h",
"badging/badge_manager_delegate.cc",
"badging/badge_manager_delegate.h",
"badging/badge_manager_factory.cc",
"badging/badge_manager_factory.h",
......
......@@ -9,11 +9,13 @@
#include "base/i18n/number_formatting.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "ui/base/l10n/l10n_util.h"
......@@ -27,23 +29,15 @@
namespace badging {
std::string GetBadgeString(base::Optional<uint64_t> badge_content) {
if (!badge_content)
return "•";
if (badge_content > kMaxBadgeContent) {
return base::UTF16ToUTF8(l10n_util::GetStringFUTF16(
IDS_SATURATED_BADGE_CONTENT, base::FormatNumber(kMaxBadgeContent)));
}
return base::UTF16ToUTF8(base::FormatNumber(badge_content.value()));
}
BadgeManager::BadgeManager(Profile* profile) {
#if defined(OS_MACOSX)
SetDelegate(std::make_unique<BadgeManagerDelegateMac>(profile));
SetDelegate(std::make_unique<BadgeManagerDelegateMac>(
profile, this,
&web_app::WebAppProviderBase::GetProviderBase(profile)->registrar()));
#elif defined(OS_WIN)
SetDelegate(std::make_unique<BadgeManagerDelegateWin>(profile));
SetDelegate(std::make_unique<BadgeManagerDelegateWin>(
profile, this,
&web_app::WebAppProviderBase::GetProviderBase(profile)->registrar()));
#endif
}
......@@ -66,79 +60,107 @@ void BadgeManager::BindRequest(
std::move(context));
}
void BadgeManager::UpdateAppBadge(const base::Optional<std::string>& app_id,
base::Optional<uint64_t> content) {
// Badge content should never be 0 (it should be translated into a clear).
DCHECK_NE(content.value_or(1), 0u);
bool BadgeManager::HasMoreSpecificBadgeForUrl(const GURL& scope,
const GURL& url) {
return MostSpecificBadgeForScope(url).spec().size() > scope.spec().size();
}
if (!app_id) {
BadgeChangeIgnored();
return;
}
base::Optional<BadgeManager::BadgeValue> BadgeManager::GetBadgeValue(
const GURL& scope) {
const GURL& most_specific = MostSpecificBadgeForScope(scope);
if (most_specific == GURL::EmptyGURL())
return base::nullopt;
badged_apps_[app_id.value()] = content;
return base::make_optional(badged_scopes_[most_specific]);
}
void BadgeManager::SetBadgeForTesting(const GURL& scope, BadgeValue value) {
UpdateBadge(scope, value);
}
void BadgeManager::ClearBadgeForTesting(const GURL& scope) {
UpdateBadge(scope, base::nullopt);
}
void BadgeManager::UpdateBadge(const GURL& scope,
base::Optional<BadgeValue> value) {
if (!value)
badged_scopes_.erase(scope);
else
badged_scopes_[scope] = value.value();
if (!delegate_)
return;
delegate_->OnBadgeSet(app_id.value(), content);
delegate_->OnBadgeUpdated(scope);
}
void BadgeManager::ClearAppBadge(const base::Optional<std::string>& app_id) {
if (!app_id) {
BadgeChangeIgnored();
void BadgeManager::SetBadge(const GURL& scope,
blink::mojom::BadgeValuePtr mojo_value) {
if (mojo_value->is_number() && mojo_value->get_number() == 0) {
mojo::ReportBadMessage(
"|value| should not be zero when it is |number| (ClearBadge should be "
"called instead)!");
return;
}
badged_apps_.erase(app_id.value());
if (!delegate_)
if (!ScopeIsValidBadgeTarget(receivers_.current_context(), scope))
return;
delegate_->OnBadgeCleared(app_id.value());
// Convert the mojo badge representation into a BadgeManager::BadgeValue.
BadgeValue value = mojo_value->is_flag()
? base::nullopt
: base::make_optional(mojo_value->get_number());
UpdateBadge(scope, base::make_optional(value));
}
void BadgeManager::BadgeChangeIgnored() {
if (!delegate_)
void BadgeManager::ClearBadge(const GURL& scope) {
if (!ScopeIsValidBadgeTarget(receivers_.current_context(), scope))
return;
delegate_->OnBadgeChangeIgnoredForTesting();
UpdateBadge(scope, base::nullopt);
}
void BadgeManager::SetInteger(uint64_t content) {
UpdateAppBadge(GetAppIdToBadge(receivers_.current_context()), content);
}
GURL BadgeManager::MostSpecificBadgeForScope(const GURL& scope) {
const std::string& scope_string = scope.spec();
GURL best_match = GURL::EmptyGURL();
uint64_t longest_match = 0;
void BadgeManager::SetFlag() {
UpdateAppBadge(GetAppIdToBadge(receivers_.current_context()), base::nullopt);
}
for (const auto& pair : badged_scopes_) {
const std::string& cur_scope_str = pair.first.spec();
if (scope_string.find(cur_scope_str) != 0)
continue;
void BadgeManager::ClearBadge() {
ClearAppBadge(GetAppIdToBadge(receivers_.current_context()));
if (longest_match >= cur_scope_str.size())
continue;
longest_match = cur_scope_str.size();
best_match = pair.first;
}
return best_match;
}
base::Optional<std::string> BadgeManager::GetAppIdToBadge(
const BindingContext& context) {
bool BadgeManager::ScopeIsValidBadgeTarget(const BindingContext& context,
const GURL& scope) {
content::RenderFrameHost* frame =
content::RenderFrameHost::FromID(context.process_id, context.frame_id);
if (!frame)
return base::nullopt;
return false;
content::WebContents* contents =
content::WebContents::FromRenderFrameHost(frame);
Browser* browser = chrome::FindBrowserWithWebContents(contents);
if (!browser)
return base::nullopt;
return url::IsSameOriginWith(frame->GetLastCommittedURL(), scope);
}
web_app::AppBrowserController* app_controller = browser->app_controller();
if (!app_controller)
return base::nullopt;
std::string GetBadgeString(base::Optional<uint64_t> badge_content) {
if (!badge_content)
return "•";
// If the frame is not in scope, don't apply a badge.
if (!app_controller->IsUrlInAppScope(frame->GetLastCommittedURL())) {
return base::nullopt;
if (badge_content > kMaxBadgeContent) {
return base::UTF16ToUTF8(l10n_util::GetStringFUTF16(
IDS_SATURATED_BADGE_CONTENT, base::FormatNumber(kMaxBadgeContent)));
}
return app_controller->GetAppId();
return base::UTF16ToUTF8(base::FormatNumber(badge_content.value()));
}
} // namespace badging
......@@ -11,10 +11,10 @@
#include "base/macros.h"
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "components/keyed_service/core/keyed_service.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "third_party/blink/public/mojom/badging/badging.mojom.h"
#include "url/gurl.h"
class Profile;
......@@ -23,17 +23,19 @@ class RenderFrameHost;
} // namespace content
namespace badging {
class BadgeManagerDelegate;
// The maximum value of badge contents before saturation occurs.
constexpr uint64_t kMaxBadgeContent = 99u;
// Determines the text to put on the badge based on some badge_content.
std::string GetBadgeString(base::Optional<uint64_t> badge_content);
// Maintains a record of badge contents and dispatches badge changes to a
// delegate.
class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
public:
// The badge being applied to a URL. If the optional is |base::nullopt| then
// the badge is "flag". Otherwise the badge is a non-zero integer.
using BadgeValue = base::Optional<uint64_t>;
explicit BadgeManager(Profile* profile);
~BadgeManager() override;
......@@ -44,13 +46,16 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
mojo::PendingReceiver<blink::mojom::BadgeService> receiver,
content::RenderFrameHost* frame);
// Sets the badge for |app_id| to be |content|. Note: If content is set, it
// must be non-zero.
void UpdateAppBadge(const base::Optional<std::string>& app_id,
base::Optional<uint64_t> content);
// Returns whether there is a more specific badge for |url| than |scope|.
// Note: This function does not check that there is a badge for |scope|.
bool HasMoreSpecificBadgeForUrl(const GURL& scope, const GURL& url);
// Gets the most specific badge applying to |scope|. This will be
// base::nullopt if the scope is not badged.
base::Optional<BadgeValue> GetBadgeValue(const GURL& scope);
// Clears the badge for |app_id|.
void ClearAppBadge(const base::Optional<std::string>& app_id);
void SetBadgeForTesting(const GURL& scope, BadgeValue value);
void ClearBadgeForTesting(const GURL& scope);
private:
// The BindingContext of a mojo request. Allows mojo calls to be tied back to
......@@ -63,18 +68,23 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
int frame_id;
};
// Notifies |delegate_| that a badge change was ignored.
void BadgeChangeIgnored();
// Updates the badge for |scope| to be |value|, if it is not base::nullopt.
// If value is |base::nullopt| then this clears the badge.
void UpdateBadge(const GURL& scope, base::Optional<BadgeValue> value);
// blink::mojom::BadgeService:
// Note: These are private to stop them being called outside of mojo as they
// require a mojo binding context.
void SetInteger(uint64_t content) override;
void SetFlag() override;
void ClearBadge() override;
void SetBadge(const GURL& scope, blink::mojom::BadgeValuePtr value) override;
void ClearBadge(const GURL& scope) override;
// Examines |context| to determine which app, if any, should be badged.
base::Optional<std::string> GetAppIdToBadge(const BindingContext& context);
// Finds the scope URL of the most specific badge for |scope|. Returns
// GURL::EmptyGURL() if no match is found.
GURL MostSpecificBadgeForScope(const GURL& scope);
// Determines whether |context| is allowed to change the badge for |scope|.
bool ScopeIsValidBadgeTarget(const BindingContext& context,
const GURL& scope);
// All the mojo receivers for the BadgeManager. Keeps track of the
// render_frame the binding is associated with, so as to not have to rely
......@@ -85,12 +95,15 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
// Note: This is currently only set on Windows and MacOS.
std::unique_ptr<BadgeManagerDelegate> delegate_;
// Maps app id to badge contents.
std::map<std::string, base::Optional<uint64_t>> badged_apps_;
// Maps scope to badge contents.
std::map<GURL, BadgeValue> badged_scopes_;
DISALLOW_COPY_AND_ASSIGN(BadgeManager);
};
// Determines the text to put on the badge based on some badge_content.
std::string GetBadgeString(BadgeManager::BadgeValue badge_content);
} // namespace badging
#endif // CHROME_BROWSER_BADGING_BADGE_MANAGER_H_
// 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 "chrome/browser/badging/badge_manager_delegate.h"
#include <vector>
#include "chrome/browser/web_applications/components/app_registrar.h"
namespace badging {
void BadgeManagerDelegate::OnBadgeUpdated(const GURL& scope) {
const std::vector<web_app::AppId>& app_ids =
registrar()->FindAppsInScope(scope);
for (const auto& app_id : app_ids) {
const auto& app_scope = registrar()->GetAppScope(app_id);
if (!app_scope)
continue;
// If it wasn't the most specific badge for the app that changed, there's no
// need to update it.
if (badge_manager()->HasMoreSpecificBadgeForUrl(scope, app_scope.value()))
continue;
OnAppBadgeUpdated(app_id);
}
}
base::Optional<BadgeManager::BadgeValue> BadgeManagerDelegate::GetAppBadgeValue(
const web_app::AppId& app_id) {
const auto& scope = registrar()->GetAppScope(app_id);
if (!scope)
return base::nullopt;
return badge_manager()->GetBadgeValue(scope.value());
}
} // namespace badging
......@@ -5,36 +5,56 @@
#ifndef CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_
#define CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_
#include <string>
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "url/gurl.h"
class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging {
// BadgeManagerDelegate is responsible for dispatching badge events that should
// be handled and reflected in the UI.
// A BadgeManagerDelegate is responsible for updating the UI in response to a
// badge change.
class BadgeManagerDelegate {
public:
explicit BadgeManagerDelegate(Profile* profile) : profile_(profile) {}
explicit BadgeManagerDelegate(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: profile_(profile),
badge_manager_(badge_manager),
registrar_(registrar) {}
virtual ~BadgeManagerDelegate() = default;
virtual ~BadgeManagerDelegate() {}
// Called when the badge for |scope| has changed.
virtual void OnBadgeUpdated(const GURL& scope);
// Called when an app's badge has changed.
virtual void OnBadgeSet(const std::string& app_id,
base::Optional<uint64_t> contents) = 0;
protected:
// Called when the badge for |app_id| has changed.
virtual void OnAppBadgeUpdated(const web_app::AppId& app_id) = 0;
// Called when a app's badge has been cleared.
virtual void OnBadgeCleared(const std::string& app_id) = 0;
// Gets the badge for |app_id|. base::nullopt if the |app_id| is not badged.
base::Optional<BadgeManager::BadgeValue> GetAppBadgeValue(
const web_app::AppId& app_id);
// Called when a page attempts to set or clear a badge but the badge service
// determines that the page should not able to change the badge.
virtual void OnBadgeChangeIgnoredForTesting() {}
Profile* profile() { return profile_; }
BadgeManager* badge_manager() { return badge_manager_; }
web_app::AppRegistrar* registrar() { return registrar_; }
protected:
private:
// The profile the badge manager delegate is associated with.
Profile* profile_;
// The badge manager that owns this delegate.
BadgeManager* badge_manager_;
// The registrar of apps this delegate is concerned with.
web_app::AppRegistrar* registrar_;
DISALLOW_COPY_AND_ASSIGN(BadgeManagerDelegate);
};
} // namespace badging
......
......@@ -9,22 +9,21 @@
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/common/mac/app_shim.mojom.h"
namespace {} // namespace
namespace badging {
BadgeManagerDelegateMac::BadgeManagerDelegateMac(Profile* profile)
: BadgeManagerDelegate(profile) {}
void BadgeManagerDelegateMac::OnBadgeSet(const std::string& app_id,
base::Optional<uint64_t> contents) {
SetAppBadgeLabel(app_id, badging::GetBadgeString(contents));
}
BadgeManagerDelegateMac::BadgeManagerDelegateMac(
Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: BadgeManagerDelegate(profile, badge_manager, registrar) {}
void BadgeManagerDelegateMac::OnBadgeCleared(const std::string& app_id) {
SetAppBadgeLabel(app_id, "");
void BadgeManagerDelegateMac::OnAppBadgeUpdated(const web_app::AppId& app_id) {
const base::Optional<BadgeManager::BadgeValue>& badge =
GetAppBadgeValue(app_id);
SetAppBadgeLabel(app_id, badge ? badging::GetBadgeString(badge.value()) : "");
}
void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id,
......@@ -33,9 +32,9 @@ void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id,
if (!shim_handler)
return;
// On OSX all app instances share a dock icon, so we only need to set the
// On macOS all app instances share a dock icon, so we only need to set the
// badge label once.
AppShimHost* shim_host = shim_handler->FindHost(profile_, app_id);
AppShimHost* shim_host = shim_handler->FindHost(profile(), app_id);
if (!shim_host)
return;
......
......@@ -12,17 +12,22 @@
class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging {
class BadgeManager;
// OSX specific implementation of the BadgeManagerDelegate.
class BadgeManagerDelegateMac : public BadgeManagerDelegate {
public:
explicit BadgeManagerDelegateMac(Profile* profile);
void OnBadgeSet(const std::string& app_id,
base::Optional<uint64_t> contents) override;
explicit BadgeManagerDelegateMac(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar);
void OnBadgeCleared(const std::string& app_id) override;
void OnAppBadgeUpdated(const web_app::AppId& app_id) override;
private:
void SetAppBadgeLabel(const std::string& app_id,
......
// 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 <memory>
#include <utility>
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/badging/test_badge_manager_delegate.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/test/test_app_registrar.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace badging {
class BadgeManagerDelegateUnittest : public ::testing::Test {
public:
BadgeManagerDelegateUnittest() = default;
~BadgeManagerDelegateUnittest() override = default;
void SetUp() override {
profile_.reset(new TestingProfile());
registrar_.reset(new web_app::TestAppRegistrar());
badge_manager_ =
BadgeManagerFactory::GetInstance()->GetForProfile(profile_.get());
// Delegate lifetime is managed by BadgeManager.
auto owned_delegate = std::make_unique<TestBadgeManagerDelegate>(
profile_.get(), badge_manager_, registrar());
delegate_ = owned_delegate.get();
badge_manager_->SetDelegate(std::move(owned_delegate));
}
void TearDown() override { profile_.reset(); }
TestBadgeManagerDelegate* delegate() { return delegate_; }
web_app::TestAppRegistrar* registrar() const { return registrar_.get(); }
BadgeManager* badge_manager() const { return badge_manager_; }
private:
std::unique_ptr<web_app::TestAppRegistrar> registrar_;
TestBadgeManagerDelegate* delegate_;
BadgeManager* badge_manager_;
std::unique_ptr<TestingProfile> profile_;
content::BrowserTaskEnvironment task_environment_;
DISALLOW_COPY_AND_ASSIGN(BadgeManagerDelegateUnittest);
};
TEST_F(BadgeManagerDelegateUnittest, InScopeAppsAreBadged) {
const web_app::AppId& kApp1 = "app1";
const web_app::AppId& kApp2 = "app2";
registrar()->AddExternalApp(kApp1, {GURL("https://example.com/app1")});
registrar()->AddExternalApp(kApp2, {GURL("https://example.com/app2")});
badge_manager()->SetBadgeForTesting(GURL("https://example.com/"),
base::nullopt);
EXPECT_EQ(2UL, delegate()->set_app_badges().size());
EXPECT_EQ(kApp1, delegate()->set_app_badges()[0].first);
EXPECT_EQ(base::nullopt, delegate()->set_app_badges()[0].second);
EXPECT_EQ(kApp2, delegate()->set_app_badges()[1].first);
EXPECT_EQ(base::nullopt, delegate()->set_app_badges()[1].second);
}
TEST_F(BadgeManagerDelegateUnittest, InScopeAppsAreCleared) {
const web_app::AppId& kApp1 = "app1";
const web_app::AppId& kApp2 = "app2";
registrar()->AddExternalApp(kApp1, {GURL("https://example.com/app1")});
registrar()->AddExternalApp(kApp2, {GURL("https://example.com/app2")});
badge_manager()->SetBadgeForTesting(GURL("https://example.com/"),
base::nullopt);
badge_manager()->ClearBadgeForTesting(GURL("https://example.com/"));
EXPECT_EQ(2UL, delegate()->cleared_app_badges().size());
EXPECT_EQ(kApp1, delegate()->cleared_app_badges()[0]);
EXPECT_EQ(kApp2, delegate()->cleared_app_badges()[1]);
}
} // namespace badging
......@@ -12,30 +12,37 @@
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/strings/grit/ui_strings.h"
namespace badging {
BadgeManagerDelegateWin::BadgeManagerDelegateWin(Profile* profile)
: BadgeManagerDelegate(profile) {}
namespace {
void BadgeManagerDelegateWin::OnBadgeSet(const std::string& app_id,
base::Optional<uint64_t> contents) {
auto badge_string = badging::GetBadgeString(contents);
// There are 3 different cases:
// Determines the badge contents and alt text.
// base::nullopt if the badge is not set.
// otherwise a pair (badge_content, badge_alt_text), based on the content of the
// badge.
base::Optional<std::pair<std::string, std::string>> GetBadgeContentAndAlt(
const base::Optional<BadgeManager::BadgeValue>& badge) {
// If there is no badge, there is no contents or alt text.
if (!badge)
return base::nullopt;
std::string badge_string = badging::GetBadgeString(badge.value());
// There are 3 different cases when the badge has a value:
// 1. |contents| is between 1 and 99 inclusive => Set the accessibility text
// to a pluralized notification count (e.g. 4 Unread Notifications).
// 2. |contents| is greater than 99 => Set the accessibility text to
// More than |kMaxBadgeContent| unread notifications, so the
// accessibility text matches what is displayed on the badge (e.g. More
// than 99 notifications).
// 3. |contents| doesn't have a value (i.e. the badge is set to 'flag') => Set
// the accessibility text to something less specific (e.g. Unread
// Notifications).
// 3. The badge is set to 'flag' => Set the accessibility text to something
// less specific (e.g. Unread Notifications).
std::string badge_alt_string;
if (contents) {
uint64_t value = contents.value();
if (badge.value()) {
uint64_t value = badge.value().value();
badge_alt_string = value <= badging::kMaxBadgeContent
// Case 1.
? l10n_util::GetPluralStringFUTF8(
......@@ -50,25 +57,31 @@ void BadgeManagerDelegateWin::OnBadgeSet(const std::string& app_id,
l10n_util::GetStringUTF8(IDS_BADGE_UNREAD_NOTIFICATIONS_UNSPECIFIED);
}
for (Browser* browser : *BrowserList::GetInstance()) {
if (!IsAppBrowser(browser, app_id))
continue;
auto* window = browser->window()->GetNativeWindow();
taskbar::DrawTaskbarDecorationString(window, badge_string,
badge_alt_string);
}
return std::make_pair(badge_string, badge_alt_string);
}
void BadgeManagerDelegateWin::OnBadgeCleared(const std::string& app_id) {
} // namespace
BadgeManagerDelegateWin::BadgeManagerDelegateWin(
Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: BadgeManagerDelegate(profile, badge_manager, registrar) {}
void BadgeManagerDelegateWin::OnAppBadgeUpdated(const web_app::AppId& app_id) {
const auto& content_and_alt = GetBadgeContentAndAlt(GetAppBadgeValue(app_id));
for (Browser* browser : *BrowserList::GetInstance()) {
if (!IsAppBrowser(browser, app_id))
continue;
// Restore the decoration to whatever it is naturally (either nothing or a
// profile picture badge).
taskbar::UpdateTaskbarDecoration(browser->profile(),
browser->window()->GetNativeWindow());
auto* window = browser->window()->GetNativeWindow();
if (content_and_alt) {
taskbar::DrawTaskbarDecorationString(window, content_and_alt->first,
content_and_alt->second);
} else {
taskbar::UpdateTaskbarDecoration(browser->profile(), window);
}
}
}
......@@ -76,7 +89,7 @@ bool BadgeManagerDelegateWin::IsAppBrowser(Browser* browser,
const std::string& app_id) {
return browser->app_controller() &&
browser->app_controller()->GetAppId() == app_id &&
browser->profile() == profile_;
browser->profile() == profile();
}
} // namespace badging
......@@ -13,17 +13,22 @@
class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging {
class BadgeManager;
// Windows specific implementation of the BadgeManagerDelegate.
class BadgeManagerDelegateWin : public BadgeManagerDelegate {
public:
explicit BadgeManagerDelegateWin(Profile* profile);
void OnBadgeSet(const std::string& app_id,
base::Optional<uint64_t> contents) override;
explicit BadgeManagerDelegateWin(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar);
void OnBadgeCleared(const std::string& app_id) override;
void OnAppBadgeUpdated(const web_app::AppId& app_id) override;
private:
// Determines if a browser is for a specific hosted app, on this profile.
......
// 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 "chrome/browser/badging/test_badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
namespace badging {
TestBadgeManagerDelegate::TestBadgeManagerDelegate(
Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: BadgeManagerDelegate(profile, badge_manager, registrar) {}
TestBadgeManagerDelegate::~TestBadgeManagerDelegate() = default;
void TestBadgeManagerDelegate::SetOnBadgeChanged(
base::RepeatingCallback<void()> on_badge_changed) {
on_badge_changed_ = on_badge_changed;
}
void TestBadgeManagerDelegate::OnBadgeUpdated(const GURL& scope) {
BadgeManagerDelegate::OnBadgeUpdated(scope);
const auto& value = badge_manager()->GetBadgeValue(scope);
if (!value)
cleared_scope_badges_.push_back(scope);
else
set_scope_badges_.push_back(std::make_pair(scope, value.value()));
if (on_badge_changed_)
on_badge_changed_.Run();
}
void TestBadgeManagerDelegate::OnAppBadgeUpdated(const web_app::AppId& app_id) {
const auto& value = GetAppBadgeValue(app_id);
if (!value)
cleared_app_badges_.push_back(app_id);
else
set_app_badges_.push_back(std::make_pair(app_id, value.value()));
}
void TestBadgeManagerDelegate::ResetBadges() {
cleared_app_badges_.clear();
set_app_badges_.clear();
cleared_scope_badges_.clear();
set_scope_badges_.clear();
}
} // namespace badging
// 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.
#ifndef CHROME_BROWSER_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
#define CHROME_BROWSER_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
#include <vector>
#include "base/callback.h"
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging {
class BadgeManager;
using AppBadge = std::pair<web_app::AppId, BadgeManager::BadgeValue>;
using ScopeBadge = std::pair<GURL, BadgeManager::BadgeValue>;
// Testing delegate that records badge changes for apps.
class TestBadgeManagerDelegate : public BadgeManagerDelegate {
public:
TestBadgeManagerDelegate(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar);
~TestBadgeManagerDelegate() override;
// Sets a callback to fire when a badge is set or cleared.
void SetOnBadgeChanged(base::RepeatingCallback<void()> on_badge_changed);
// Resets the lists of cleared and set badges.
void ResetBadges();
std::vector<web_app::AppId> cleared_app_badges() {
return cleared_app_badges_;
}
std::vector<AppBadge> set_app_badges() { return set_app_badges_; }
std::vector<GURL> cleared_scope_badges() { return cleared_scope_badges_; }
std::vector<ScopeBadge> set_scope_badges() { return set_scope_badges_; }
// BadgeManagerDelegate:
void OnBadgeUpdated(const GURL& scope) override;
protected:
// BadgeManagerDelegate:
void OnAppBadgeUpdated(const web_app::AppId& app_badge) override;
private:
base::RepeatingCallback<void()> on_badge_changed_;
std::vector<web_app::AppId> cleared_app_badges_;
std::vector<AppBadge> set_app_badges_;
std::vector<GURL> cleared_scope_badges_;
std::vector<ScopeBadge> set_scope_badges_;
};
} // namespace badging
#endif // CHROME_BROWSER_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
......@@ -3,10 +3,11 @@
// found in the LICENSE file.
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/badging/test_badge_manager_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "content/public/browser/web_contents.h"
using content::RenderFrameHost;
......@@ -16,40 +17,9 @@ namespace web_app {
class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
public:
// Listens to BadgeManager events and forwards them to the test class.
class TestBadgeManagerDelegate : public badging::BadgeManagerDelegate {
public:
using SetBadgeCallback =
base::RepeatingCallback<void(const AppId&, base::Optional<uint64_t>)>;
using ClearBadgeCallback = base::RepeatingCallback<void(const AppId&)>;
using ChangeFailedCallback = base::RepeatingCallback<void()>;
TestBadgeManagerDelegate(Profile* profile,
SetBadgeCallback on_set_badge,
ClearBadgeCallback on_clear_badge,
ChangeFailedCallback on_change_failed)
: badging::BadgeManagerDelegate(profile),
on_set_badge_(on_set_badge),
on_clear_badge_(on_clear_badge),
on_change_failed_(on_change_failed) {}
void OnBadgeSet(const AppId& app_id,
base::Optional<uint64_t> contents) override {
on_set_badge_.Run(app_id, contents);
}
void OnBadgeCleared(const AppId& app_id) override {
on_clear_badge_.Run(app_id);
}
void OnBadgeChangeIgnoredForTesting() override { on_change_failed_.Run(); }
private:
SetBadgeCallback on_set_badge_;
ClearBadgeCallback on_clear_badge_;
ChangeFailedCallback on_change_failed_;
};
WebAppBadgingBrowserTest()
: WebAppControllerBrowserTest(),
cross_origin_https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
WebAppControllerBrowserTest::SetUpCommandLine(command_line);
......@@ -59,25 +29,33 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
void SetUpOnMainThread() override {
WebAppControllerBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(cross_origin_https_server_.Start());
ASSERT_TRUE(https_server()->Start());
ASSERT_TRUE(embedded_test_server()->Start());
AppId app_id = InstallPWA(https_server()->GetURL(
"/ssl/page_with_in_scope_and_cross_site_frame.html"));
GURL cross_site_frame_url =
cross_origin_https_server_.GetURL("/ssl/google.html");
// Note: The url for the cross site frame is embedded in the query string.
GURL app_url = https_server()->GetURL(
"/ssl/page_with_in_scope_and_cross_site_frame.html?url=" +
cross_site_frame_url.spec());
AppId app_id = InstallPWA(app_url);
content::WebContents* web_contents = OpenApplication(app_id);
// There should be exactly 3 frames:
// 1) The main frame.
// 2) A cross site frame, on https://example.com/
// 2) A cross site frame, on |cross_site_frame_url|.
// 3) A sub frame in the app's scope.
auto frames = web_contents->GetAllFrames();
ASSERT_EQ(3u, frames.size());
main_frame_ = web_contents->GetMainFrame();
for (auto* frame : frames) {
if (frame->GetLastCommittedURL() == "https://example.com/")
cross_site_frame_ = frame;
else if (frame != main_frame_)
if (url::IsSameOriginWith(frame->GetLastCommittedURL(),
main_frame_->GetLastCommittedURL())) {
in_scope_frame_ = frame;
} else if (frame != main_frame_) {
cross_site_frame_ = frame;
}
}
ASSERT_TRUE(main_frame_);
......@@ -86,36 +64,38 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
awaiter_ = std::make_unique<base::RunLoop>();
std::unique_ptr<badging::BadgeManagerDelegate> delegate =
std::make_unique<TestBadgeManagerDelegate>(
profile(),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeSet,
base::Unretained(this)),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeCleared,
base::Unretained(this)),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeChangeFailed,
base::Unretained(this)));
badging::BadgeManagerFactory::GetInstance()
->GetForProfile(profile())
->SetDelegate(std::move(delegate));
}
badging::BadgeManager* badge_manager =
badging::BadgeManagerFactory::GetInstance()->GetForProfile(profile());
void OnBadgeSet(const AppId& app_id, base::Optional<uint64_t> badge_content) {
if (badge_content.has_value())
last_badge_content_ = badge_content;
else
was_flagged_ = true;
// The delegate is owned by the badge manager. We hold a pointer to it for
// the test.
std::unique_ptr<badging::TestBadgeManagerDelegate> owned_delegate =
std::make_unique<badging::TestBadgeManagerDelegate>(
profile(), badge_manager,
&WebAppProviderBase::GetProviderBase(profile())->registrar());
owned_delegate->SetOnBadgeChanged(base::BindRepeating(
&WebAppBadgingBrowserTest::OnBadgeChanged, base::Unretained(this)));
delegate_ = owned_delegate.get();
awaiter_->Quit();
badge_manager->SetDelegate(std::move(owned_delegate));
}
void OnBadgeCleared(const AppId& app_id) {
was_cleared_ = true;
awaiter_->Quit();
void OnBadgeChanged() {
// This is only set up to deal with one badge change at a time, in order to
// make asserting the result of a badge change easier.
int total_changes = delegate_->cleared_app_badges().size() +
delegate_->set_app_badges().size();
EXPECT_LE(total_changes, 1);
// A change is a failure if it doesn't set or clear any badges.
change_failed_ = total_changes == 0;
was_cleared_ = delegate_->cleared_app_badges().size();
if (delegate_->set_app_badges().size() == 1) {
last_badge_content_ = delegate_->set_app_badges()[0].second;
was_flagged_ = last_badge_content_ == base::nullopt;
}
void OnBadgeChangeFailed() {
change_failed_ = true;
awaiter_->Quit();
}
......@@ -127,6 +107,7 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
change_failed_ = false;
last_badge_content_ = base::nullopt;
awaiter_ = std::make_unique<base::RunLoop>();
delegate_->ResetBadges();
ASSERT_TRUE(content::ExecuteScript(on, script));
......@@ -148,8 +129,29 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
private:
std::unique_ptr<base::RunLoop> awaiter_;
badging::TestBadgeManagerDelegate* delegate_;
net::EmbeddedTestServer cross_origin_https_server_;
};
// Tests that the badge for the main frame is not affected by changing the badge
// of a cross site subframe.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
BadgeCannotBeChangedFromCrossSiteFrame) {
// Clearing from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.clear()",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
// Setting from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.set(77)",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
}
// Tests that setting the badge to an integer will be propagated across
// processes.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest, BadgeCanBeSetToAnInteger) {
......@@ -213,24 +215,6 @@ IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
ASSERT_EQ(base::nullopt, last_badge_content_);
}
// Tests that the badge cannot be set and cleared from a cross site frame.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
BadgeCannotBeChangedFromCrossSiteFrame) {
// Clearing from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.clear()",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
// Setting from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.set(77)",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
WebAppBadgingBrowserTest,
......
......@@ -798,6 +798,8 @@ if (!is_android) {
"../browser/background_fetch/background_fetch_browsertest.cc",
"../browser/background_sync/background_sync_browsertest.cc",
"../browser/background_sync/background_sync_metrics_browsertest.cc",
"../browser/badging/test_badge_manager_delegate.cc",
"../browser/badging/test_badge_manager_delegate.h",
"../browser/banners/app_banner_manager_browsertest.cc",
"../browser/banners/app_banner_manager_browsertest_base.cc",
"../browser/banners/app_banner_manager_browsertest_base.h",
......@@ -3594,7 +3596,10 @@ test("unit_tests") {
# !is_android
sources += [
# Badging isn't supported on Android.
"../browser/badging/badge_manager_delegate_unittest.cc",
"../browser/badging/badge_manager_unittest.cc",
"../browser/badging/test_badge_manager_delegate.cc",
"../browser/badging/test_badge_manager_delegate.h",
# Bookmark export/import are handled via the BookmarkColumns
# ContentProvider.
......
......@@ -3,6 +3,11 @@
</head>
<body>
<iframe name="in-scope" src="/ssl/google.html" id="in-scope"></iframe>
<iframe name="cross-site" src="https://example.com" id="cross-site"></iframe>
<iframe name="cross-site" id="cross-site"></iframe>
<script type="text/javascript">
const crossFrame = document.getElementById('cross-site');
const crossSiteUrl = new URLSearchParams(location.search).get('url');
crossFrame.src = crossSiteUrl;
</script>
</body>
</html>
......@@ -4,17 +4,28 @@
module blink.mojom;
import "url/mojom/url.mojom";
// A value that a badge can hold.
union BadgeValue {
// Represents the special value |flag|. This indicates that a badge should be
// set but that no data should be displayed in it.
// Note: The value of this field is unused (see https://crbug.com/999900).
uint8 flag;
// Represents a numerical badge. Note: This must not be zero (setting the
// badge to 0 should be translated into a clear).
uint64 number;
};
// Interface for handling badge messages from frames and subframes.
interface BadgeService {
// Sets the badge for the PWA corresponding to this request to be a
// non-zero, positive integer.
SetInteger(uint64 content);
// Sets the badge for the PWA corresponding to this request to be a
// flag marker.
SetFlag();
// Asks the browser process to set a badge.
// |scope| specifies which badges to set. Note: This must be on the same
// origin as the caller.
SetBadge(url.mojom.Url scope, BadgeValue value);
// Clears the badge (if it exists) for the PWA corresponding to
// this request.
ClearBadge();
// Asks the browser to clear a badge.
// |scope| specifies the badges to clear. Note: This must be on the same
// origin as the caller.
ClearBadge(url.mojom.Url scope);
};
......@@ -7,6 +7,7 @@
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/modules/badging/badge_options.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
......@@ -28,42 +29,62 @@ Badge* Badge::From(ExecutionContext* context) {
// static
void Badge::set(ScriptState* script_state, ExceptionState& exception_state) {
BadgeFromState(script_state)->SetFlag();
Badge::set(script_state, BadgeOptions::Create(), exception_state);
}
// static
void Badge::set(ScriptState* script_state,
const BadgeOptions* options,
ExceptionState& exception_state) {
BadgeFromState(script_state)
->SetBadge(options->scope(), mojom::blink::BadgeValue::NewFlag(0));
}
// static
void Badge::set(ScriptState* script_state,
uint64_t content,
ExceptionState& exception_state) {
if (content == 0)
BadgeFromState(script_state)->Clear();
else
BadgeFromState(script_state)->SetInteger(content);
Badge::set(script_state, content, BadgeOptions::Create(), exception_state);
}
// static
void Badge::clear(ScriptState* script_state) {
BadgeFromState(script_state)->Clear();
void Badge::set(ScriptState* script_state,
uint64_t content,
const BadgeOptions* options,
ExceptionState& exception_state) {
if (content == 0) {
Badge::clear(script_state, options);
} else {
BadgeFromState(script_state)
->SetBadge(options->scope(),
mojom::blink::BadgeValue::NewNumber(content));
}
}
void Badge::SetInteger(uint64_t content) {
badge_service_->SetInteger(content);
// static
void Badge::clear(ScriptState* script_state, const BadgeOptions* options) {
BadgeFromState(script_state)->ClearBadge(options->scope());
}
void Badge::SetFlag() {
badge_service_->SetFlag();
void Badge::SetBadge(WTF::String scope, mojom::blink::BadgeValuePtr value) {
// Resolve |scope| against the URL of the current document/worker.
KURL scope_url = KURL(execution_context_->Url(), scope);
badge_service_->SetBadge(scope_url, std::move(value));
}
void Badge::Clear() {
badge_service_->ClearBadge();
void Badge::ClearBadge(WTF::String scope) {
// Resolve |scope| against the URL of the current document/worker.
badge_service_->ClearBadge(KURL(execution_context_->Url(), scope));
}
void Badge::Trace(blink::Visitor* visitor) {
Supplement<ExecutionContext>::Trace(visitor);
ScriptWrappable::Trace(visitor);
visitor->Trace(execution_context_);
}
Badge::Badge(ExecutionContext* context) {
Badge::Badge(ExecutionContext* context) : execution_context_(context) {
context->GetInterfaceProvider()->GetInterface(
badge_service_.BindNewPipeAndPassReceiver());
DCHECK(badge_service_);
......
......@@ -9,9 +9,11 @@
#include "third_party/blink/public/mojom/badging/badging.mojom-blink.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/supplementable.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
class BadgeOptions;
class ExceptionState;
class ExecutionContext;
class ScriptState;
......@@ -30,13 +32,17 @@ class Badge final : public ScriptWrappable,
~Badge() override;
// Badge IDL interface.
static void set(ScriptState*, const BadgeOptions*, ExceptionState&);
static void set(ScriptState*,
uint64_t content,
const BadgeOptions*,
ExceptionState&);
static void set(ScriptState*, ExceptionState&);
static void set(ScriptState*, uint64_t content, ExceptionState&);
static void clear(ScriptState*);
static void clear(ScriptState*, const BadgeOptions*);
void SetInteger(uint64_t content);
void SetFlag();
void Clear();
void SetBadge(WTF::String scope, mojom::blink::BadgeValuePtr value);
void ClearBadge(WTF::String scope);
void Trace(blink::Visitor*) override;
......@@ -44,6 +50,7 @@ class Badge final : public ScriptWrappable,
static Badge* BadgeFromState(ScriptState* script_state);
mojo::Remote<blink::mojom::blink::BadgeService> badge_service_;
Member<ExecutionContext> execution_context_;
};
} // namespace blink
......
// 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.
// The options used to decide how to apply a badge.
// See the explainer https://github.com/WICG/badging/blob/master/explainer.md
dictionary BadgeOptions {
// The scope of the badge. This is resolved against the URL of the current
// page.
// https://github.com/WICG/badging/blob/master/explainer.md#badge-scope
DOMString scope = "/";
};
\ No newline at end of file
......@@ -12,6 +12,6 @@
ImplementedAs=Badge
] interface ExperimentalBadge {
[CallWith=ScriptState, RaisesException, MeasureAs=BadgeSet]
static void set(optional [EnforceRange] unsigned long long contents);
[CallWith=ScriptState, MeasureAs=BadgeClear] static void clear();
static void set(optional [EnforceRange] unsigned long long contents, optional BadgeOptions options);
[CallWith=ScriptState, MeasureAs=BadgeClear] static void clear(optional BadgeOptions options);
};
......@@ -572,6 +572,7 @@ modules_dictionary_idl_files =
"background_sync/background_sync_options.idl",
"background_sync/sync_event_init.idl",
"background_sync/periodic_sync_event_init.idl",
"badging/badge_options.idl",
"bluetooth/bluetooth_advertising_event_init.idl",
"bluetooth/bluetooth_le_scan_filter_init.idl",
"bluetooth/bluetooth_le_scan_options.idl",
......
......@@ -11,9 +11,9 @@
<body>
<script>
badge_test(() => { ExperimentalBadge.set(-1); }, 'setInteger', 'TypeError');
badge_test(() => { ExperimentalBadge.set(-1); }, undefined, 'TypeError');
badge_test(() => { ExperimentalBadge.set("Foo"); }, 'setInteger', 'TypeError');
badge_test(() => { ExperimentalBadge.set("Foo"); }, undefined, 'TypeError');
</script>
</body>
......
......@@ -11,16 +11,16 @@
<body>
<script>
badge_test(() => { ExperimentalBadge.set(); }, 'setFlag');
badge_test(() => { ExperimentalBadge.set(); }, 'flag');
badge_test(() => { ExperimentalBadge.set(undefined); }, 'setFlag');
badge_test(() => { ExperimentalBadge.set(undefined); }, 'flag');
badge_test(() => { ExperimentalBadge.set(1); }, 'setInteger');
badge_test(() => { ExperimentalBadge.set(1); }, 'number');
// Setting the Badge to 0 should be equivalent to clearing the badge.
badge_test(() => { ExperimentalBadge.set(0); }, 'clearBadge');
badge_test(() => { ExperimentalBadge.set(0); }, 'clear');
badge_test(() => { ExperimentalBadge.clear(); }, 'clearBadge');
badge_test(() => { ExperimentalBadge.clear(); }, 'clear');
</script>
</body>
......
......@@ -7,8 +7,8 @@
<script type="text/plain" id="tested">
interface ExperimentalBadge {
[CallWith=ScriptState, RaisesException]
static void set(optional [EnforceRange] unsigned long long contents);
[CallWith=ScriptState] static void clear();
static void set(optional [EnforceRange] unsigned long long contents, optional BadgeOptions options);
[CallWith=ScriptState] static void clear(optional BadgeOptions options);
};
</script>
<script>
......
......@@ -10,35 +10,36 @@ class MockBadgeService {
this.interceptor_.start();
}
init_(expectCalled) {
this.expectCalled_ = expectCalled;
init_(expectedAction) {
this.expectedAction = expectedAction;
return new Promise((resolve, reject) => {
this.reject_ = reject;
this.resolve_ = resolve;
});
}
setInteger(contents) {
setBadge(scope, value) {
// Accessing number when the union is a flag will throw, so read the
// value in a try catch.
let number;
try {
assert_equals(this.expectCalled_, 'setInteger');
this.resolve_();
number = value.number;
} catch (error) {
this.reject_(error);
}
number = undefined;
}
setFlag() {
try {
assert_equals(this.expectCalled_, 'setFlag');
const action = number === undefined ? 'flag' : 'number';
assert_equals(this.expectedAction, action);
this.resolve_();
} catch (error) {
this.reject(error);
this.reject_();
}
}
clearBadge() {
clearBadge(scope) {
try {
assert_equals(this.expectCalled_, 'clearBadge');
assert_equals(this.expectedAction, 'clear');
this.resolve_();
} catch (error) {
this.reject_(error);
......@@ -63,9 +64,9 @@ function callAndObserveErrors(func, expectedErrorName) {
});
}
function badge_test(func, expectCalled, expectError) {
function badge_test(func, expectedAction, expectError) {
promise_test(() => {
let mockPromise = mockBadgeService.init_(expectCalled);
let mockPromise = mockBadgeService.init_(expectedAction);
return Promise.race([callAndObserveErrors(func, expectError), mockPromise]);
});
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment