Browse Source

fix: enable navigator.setAppBadge/clearAppBadge (#27067)

John Kleinschmidt 4 years ago
parent
commit
c5a41defbd

+ 2 - 2
docs/api/app.md

@@ -1174,9 +1174,9 @@ For `infoType` equal to `basic`:
 
 Using `basic` should be preferred if only basic information like `vendorId` or `driverId` is needed.
 
-### `app.setBadgeCount(count)` _Linux_ _macOS_
+### `app.setBadgeCount([count])` _Linux_ _macOS_
 
-* `count` Integer
+* `count` Integer (optional) - If a value is provided, set the badge to the provided value otherwise, on macOS, display a plain white dot (e.g. unknown number of notifications). On Linux, if a value is not provided the badge will not display.
 
 Returns `Boolean` - Whether the call succeeded.
 

+ 14 - 1
electron_strings.grdp

@@ -83,5 +83,18 @@
   <message name="IDS_DOWNLOAD_MORE_ACTIONS"
           desc="Tooltip of a button on the downloads page that shows a menu with actions like 'Open downloads folder' or 'Clear all'">
     More actions
-</message>
+  </message>
+  <!-- Badging -->
+  <message name="IDS_SATURATED_BADGE_CONTENT" desc="The content to display when the application's badge is too large to display to indicate that the badge is more than a given maximum. This string should be as short as possible, preferably only one character beyond the content">
+    <ph name="MAXIMUM_VALUE">$1<ex>99</ex></ph>+
+  </message>
+  <message name="IDS_BADGE_UNREAD_NOTIFICATIONS_SATURATED" desc="The accessibility text which will be read by a screen reader when the notification count is too large to display (e.g. greater than 99).">
+    {MAX_UNREAD_NOTIFICATIONS, plural, =1 {More than 1 unread notification} other {More than # unread notifications}}
+  </message>
+  <message name="IDS_BADGE_UNREAD_NOTIFICATIONS_UNSPECIFIED" desc="The accessibility text which will be read by a screen reader when there are some unspecified number of notifications, or user attention is required">
+    Unread Notifications
+  </message>
+  <message name="IDS_BADGE_UNREAD_NOTIFICATIONS" desc="The accessibility text which will be read by a screen reader when there are notifcatications">
+    {UNREAD_NOTIFICATIONS, plural, =1 {1 Unread Notification} other {# Unread Notifications}}
+  </message>  
 </grit-part>

+ 4 - 0
filenames.gni

@@ -328,6 +328,10 @@ filenames = {
     "shell/browser/api/ui_event.h",
     "shell/browser/auto_updater.cc",
     "shell/browser/auto_updater.h",
+    "shell/browser/badging/badge_manager.cc",
+    "shell/browser/badging/badge_manager.h",
+    "shell/browser/badging/badge_manager_factory.cc",
+    "shell/browser/badging/badge_manager_factory.h",
     "shell/browser/browser.cc",
     "shell/browser/browser.h",
     "shell/browser/browser_observer.h",

+ 81 - 0
shell/browser/badging/badge_manager.cc

@@ -0,0 +1,81 @@
+// Copyright 2018 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 "shell/browser/badging/badge_manager.h"
+
+#include <tuple>
+#include <utility>
+
+#include "base/i18n/number_formatting.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/web_contents.h"
+#include "shell/browser/badging/badge_manager_factory.h"
+#include "shell/browser/browser.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/strings/grit/ui_strings.h"
+
+namespace badging {
+
+BadgeManager::BadgeManager() = default;
+BadgeManager::~BadgeManager() = default;
+
+// static
+void BadgeManager::BindFrameReceiver(
+    content::RenderFrameHost* frame,
+    mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  auto* browser_context =
+      content::WebContents::FromRenderFrameHost(frame)->GetBrowserContext();
+
+  auto* badge_manager =
+      badging::BadgeManagerFactory::GetInstance()->GetForBrowserContext(
+          browser_context);
+  if (!badge_manager)
+    return;
+
+  auto context = std::make_unique<FrameBindingContext>(
+      frame->GetProcess()->GetID(), frame->GetRoutingID());
+
+  badge_manager->receivers_.Add(badge_manager, std::move(receiver),
+                                std::move(context));
+}
+
+std::string BadgeManager::GetBadgeString(base::Optional<int> 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()));
+}
+
+void BadgeManager::SetBadge(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;
+  }
+
+  base::Optional<int> value =
+      mojo_value->is_flag() ? base::nullopt
+                            : base::make_optional(mojo_value->get_number());
+
+  electron::Browser::Get()->SetBadgeCount(value);
+}
+
+void BadgeManager::ClearBadge() {
+  electron::Browser::Get()->SetBadgeCount(0);
+}
+
+}  // namespace badging

+ 90 - 0
shell/browser/badging/badge_manager.h

@@ -0,0 +1,90 @@
+// Copyright 2018 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 SHELL_BROWSER_BADGING_BADGE_MANAGER_H_
+#define SHELL_BROWSER_BADGING_BADGE_MANAGER_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/optional.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"
+
+namespace content {
+class RenderFrameHost;
+class RenderProcessHost;
+}  // namespace content
+
+namespace badging {
+
+// The maximum value of badge contents before saturation occurs.
+constexpr int kMaxBadgeContent = 99;
+
+// Maintains a record of badge contents and dispatches badge changes to a
+// delegate.
+class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
+ public:
+  BadgeManager();
+  ~BadgeManager() override;
+
+  static void BindFrameReceiver(
+      content::RenderFrameHost* frame,
+      mojo::PendingReceiver<blink::mojom::BadgeService> receiver);
+
+  // Determines the text to put on the badge based on some badge_content.
+  static std::string GetBadgeString(base::Optional<int> badge_content);
+
+ private:
+  // The BindingContext of a mojo request. Allows mojo calls to be tied back
+  // to the execution context they belong to without trusting the renderer for
+  // that information.  This is an abstract base class that different types of
+  // execution contexts derive.
+  class BindingContext {
+   public:
+    virtual ~BindingContext() = default;
+  };
+
+  // The BindingContext for Window execution contexts.
+  class FrameBindingContext final : public BindingContext {
+   public:
+    FrameBindingContext(int process_id, int frame_id)
+        : process_id_(process_id), frame_id_(frame_id) {}
+    ~FrameBindingContext() override = default;
+
+    int GetProcessId() { return process_id_; }
+    int GetFrameId() { return frame_id_; }
+
+   private:
+    int process_id_;
+    int frame_id_;
+  };
+
+  // blink::mojom::BadgeService:
+  // Note: These are private to stop them being called outside of mojo as they
+  // require a mojo binding context.
+  void SetBadge(blink::mojom::BadgeValuePtr value) override;
+  void ClearBadge() override;
+
+  // 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
+  // on the renderer passing it in.
+  mojo::ReceiverSet<blink::mojom::BadgeService, std::unique_ptr<BindingContext>>
+      receivers_;
+
+  // Delegate which handles actual setting and clearing of the badge.
+  // Note: This is currently only set on Windows and MacOS.
+  // std::unique_ptr<BadgeManagerDelegate> delegate_;
+
+  DISALLOW_COPY_AND_ASSIGN(BadgeManager);
+};
+
+}  // namespace badging
+
+#endif  // SHELL_BROWSER_BADGING_BADGE_MANAGER_H_

+ 41 - 0
shell/browser/badging/badge_manager_factory.cc

@@ -0,0 +1,41 @@
+// Copyright 2018 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 "shell/browser/badging/badge_manager_factory.h"
+
+#include <memory>
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "shell/browser/badging/badge_manager.h"
+
+namespace badging {
+
+// static
+BadgeManager* BadgeManagerFactory::GetForBrowserContext(
+    content::BrowserContext* context) {
+  return static_cast<badging::BadgeManager*>(
+      GetInstance()->GetServiceForBrowserContext(context, true));
+}
+
+// static
+BadgeManagerFactory* BadgeManagerFactory::GetInstance() {
+  return base::Singleton<BadgeManagerFactory>::get();
+}
+
+BadgeManagerFactory::BadgeManagerFactory()
+    : BrowserContextKeyedServiceFactory(
+          "BadgeManager",
+          BrowserContextDependencyManager::GetInstance()) {}
+
+BadgeManagerFactory::~BadgeManagerFactory() {}
+
+KeyedService* BadgeManagerFactory::BuildServiceInstanceFor(
+    content::BrowserContext* context) const {
+  return new BadgeManager();
+}
+
+}  // namespace badging

+ 44 - 0
shell/browser/badging/badge_manager_factory.h

@@ -0,0 +1,44 @@
+// Copyright 2018 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 SHELL_BROWSER_BADGING_BADGE_MANAGER_FACTORY_H_
+#define SHELL_BROWSER_BADGING_BADGE_MANAGER_FACTORY_H_
+
+#include "base/macros.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace base {
+template <typename T>
+struct DefaultSingletonTraits;
+}
+
+namespace badging {
+
+class BadgeManager;
+
+// Singleton that provides access to context specific BadgeManagers.
+class BadgeManagerFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  // Gets the BadgeManager for the specified context
+  static BadgeManager* GetForBrowserContext(content::BrowserContext* context);
+
+  // Returns the BadgeManagerFactory singleton.
+  static BadgeManagerFactory* GetInstance();
+
+ private:
+  friend struct base::DefaultSingletonTraits<BadgeManagerFactory>;
+
+  BadgeManagerFactory();
+  ~BadgeManagerFactory() override;
+
+  // BrowserContextKeyedServiceFactory
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* context) const override;
+
+  DISALLOW_COPY_AND_ASSIGN(BadgeManagerFactory);
+};
+
+}  // namespace badging
+
+#endif  // SHELL_BROWSER_BADGING_BADGE_MANAGER_FACTORY_H_

+ 11 - 1
shell/browser/browser.cc

@@ -23,6 +23,7 @@
 #if defined(OS_WIN)
 #include <windows.h>
 #include "base/files/file_path.h"
+#include "shell/browser/ui/win/taskbar_host.h"
 #endif
 
 #if defined(OS_MAC)
@@ -107,7 +108,7 @@ class Browser : public WindowListObserver {
 #endif
 
   // Set/Get the badge count.
-  bool SetBadgeCount(int count);
+  bool SetBadgeCount(base::Optional<int> count);
   int GetBadgeCount();
 
 #if defined(OS_WIN)
@@ -364,6 +365,15 @@ class Browser : public WindowListObserver {
   base::DictionaryValue about_panel_options_;
 #endif
 
+#if defined(OS_WIN)
+  void UpdateBadgeContents(HWND hwnd,
+                           const base::Optional<std::string>& badge_content,
+                           const std::string& badge_alt_string);
+
+  // In charge of running taskbar related APIs.
+  TaskbarHost taskbar_host_;
+#endif
+
   DISALLOW_COPY_AND_ASSIGN(Browser);
 };
 

+ 4 - 4
shell/browser/browser_linux.cc

@@ -130,10 +130,10 @@ base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
   return base::ASCIIToUTF16(GetXdgAppOutput(argv).value_or(std::string()));
 }
 
-bool Browser::SetBadgeCount(int count) {
-  if (IsUnityRunning()) {
-    unity::SetDownloadCount(count);
-    badge_count_ = count;
+bool Browser::SetBadgeCount(base::Optional<int> count) {
+  if (IsUnityRunning() && count.has_value()) {
+    unity::SetDownloadCount(count.value());
+    badge_count_ = count.value();
     return true;
   } else {
     return false;

+ 10 - 3
shell/browser/browser_mac.mm

@@ -15,6 +15,7 @@
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/sys_string_conversions.h"
 #include "net/base/mac/url_conversions.h"
+#include "shell/browser/badging/badge_manager.h"
 #include "shell/browser/mac/dict_util.h"
 #include "shell/browser/mac/electron_application.h"
 #include "shell/browser/mac/electron_application_delegate.h"
@@ -219,9 +220,15 @@ base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
 
 void Browser::SetAppUserModelID(const base::string16& name) {}
 
-bool Browser::SetBadgeCount(int count) {
-  DockSetBadgeText(count != 0 ? base::NumberToString(count) : "");
-  badge_count_ = count;
+bool Browser::SetBadgeCount(base::Optional<int> count) {
+  DockSetBadgeText(!count.has_value() || count.value() != 0
+                       ? badging::BadgeManager::GetBadgeString(count)
+                       : "");
+  if (count.has_value()) {
+    badge_count_ = count.value();
+  } else {
+    badge_count_ = 0;
+  }
   return true;
 }
 

+ 99 - 2
shell/browser/browser_win.cc

@@ -28,16 +28,21 @@
 #include "chrome/browser/icon_manager.h"
 #include "electron/electron_version.h"
 #include "shell/browser/api/electron_api_app.h"
+#include "shell/browser/badging/badge_manager.h"
 #include "shell/browser/electron_browser_main_parts.h"
 #include "shell/browser/ui/message_box.h"
 #include "shell/browser/ui/win/jump_list.h"
+#include "shell/browser/window_list.h"
 #include "shell/common/application_info.h"
 #include "shell/common/gin_converters/file_path_converter.h"
 #include "shell/common/gin_converters/image_converter.h"
 #include "shell/common/gin_helper/arguments.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/skia_util.h"
+#include "skia/ext/legacy_display_globals.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/events/keycodes/keyboard_code_conversion_win.h"
+#include "ui/strings/grit/ui_strings.h"
 
 namespace electron {
 
@@ -584,8 +589,100 @@ v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
   return handle;
 }
 
-bool Browser::SetBadgeCount(int count) {
-  return false;
+bool Browser::SetBadgeCount(base::Optional<int> count) {
+  base::Optional<std::string> badge_content;
+  if (count.has_value() && count.value() == 0) {
+    badge_content = base::nullopt;
+  } else {
+    badge_content = badging::BadgeManager::GetBadgeString(count);
+  }
+
+  // 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. The badge is set to 'flag' => Set the accessibility text to something
+  //    less specific (e.g. Unread Notifications).
+  std::string badge_alt_string;
+  if (count.has_value()) {
+    badge_count_ = count.value();
+    badge_alt_string = (uint64_t)badge_count_ <= badging::kMaxBadgeContent
+                           // Case 1.
+                           ? l10n_util::GetPluralStringFUTF8(
+                                 IDS_BADGE_UNREAD_NOTIFICATIONS, badge_count_)
+                           // Case 2.
+                           : l10n_util::GetPluralStringFUTF8(
+                                 IDS_BADGE_UNREAD_NOTIFICATIONS_SATURATED,
+                                 badging::kMaxBadgeContent);
+  } else {
+    // Case 3.
+    badge_alt_string =
+        l10n_util::GetStringUTF8(IDS_BADGE_UNREAD_NOTIFICATIONS_UNSPECIFIED);
+    badge_count_ = 0;
+  }
+  for (auto* window : WindowList::GetWindows()) {
+    // On Windows set the badge on the first window found.
+    UpdateBadgeContents(window->GetAcceleratedWidget(), badge_content,
+                        badge_alt_string);
+  }
+  return true;
+}
+
+void Browser::UpdateBadgeContents(
+    HWND hwnd,
+    const base::Optional<std::string>& badge_content,
+    const std::string& badge_alt_string) {
+  SkBitmap badge;
+  if (badge_content) {
+    std::string content = badge_content.value();
+    constexpr int kOverlayIconSize = 16;
+    // This is the color used by the Windows 10 Badge API, for platform
+    // consistency.
+    constexpr int kBackgroundColor = SkColorSetRGB(0x26, 0x25, 0x2D);
+    constexpr int kForegroundColor = SK_ColorWHITE;
+    constexpr int kRadius = kOverlayIconSize / 2;
+    // The minimum gap to have between our content and the edge of the badge.
+    constexpr int kMinMargin = 3;
+    // The amount of space we have to render the icon.
+    constexpr int kMaxBounds = kOverlayIconSize - 2 * kMinMargin;
+    constexpr int kMaxTextSize = 24;  // Max size for our text.
+    constexpr int kMinTextSize = 7;   // Min size for our text.
+
+    badge.allocN32Pixels(kOverlayIconSize, kOverlayIconSize);
+    SkCanvas canvas(badge, skia::LegacyDisplayGlobals::GetSkSurfaceProps());
+
+    SkPaint paint;
+    paint.setAntiAlias(true);
+    paint.setColor(kBackgroundColor);
+
+    canvas.clear(SK_ColorTRANSPARENT);
+    canvas.drawCircle(kRadius, kRadius, kRadius, paint);
+
+    paint.reset();
+    paint.setColor(kForegroundColor);
+
+    SkFont font;
+
+    SkRect bounds;
+    int text_size = kMaxTextSize;
+    // Find the largest |text_size| larger than |kMinTextSize| in which
+    // |content| fits into our 16x16px icon, with margins.
+    do {
+      font.setSize(text_size--);
+      font.measureText(content.c_str(), content.size(), SkTextEncoding::kUTF8,
+                       &bounds);
+    } while (text_size >= kMinTextSize &&
+             (bounds.width() > kMaxBounds || bounds.height() > kMaxBounds));
+
+    canvas.drawSimpleText(
+        content.c_str(), content.size(), SkTextEncoding::kUTF8,
+        kRadius - bounds.width() / 2 - bounds.x(),
+        kRadius - bounds.height() / 2 - bounds.y(), font, paint);
+  }
+  taskbar_host_.SetOverlayIcon(hwnd, badge, badge_alt_string);
 }
 
 void Browser::SetLoginItemSettings(LoginItemSettings settings) {

+ 2 - 7
shell/browser/electron_browser_client.cc

@@ -65,6 +65,7 @@
 #include "shell/browser/api/electron_api_session.h"
 #include "shell/browser/api/electron_api_web_contents.h"
 #include "shell/browser/api/electron_api_web_request.h"
+#include "shell/browser/badging/badge_manager.h"
 #include "shell/browser/child_web_contents_tracker.h"
 #include "shell/browser/electron_autofill_driver_factory.h"
 #include "shell/browser/electron_browser_context.h"
@@ -1622,12 +1623,6 @@ void ElectronBrowserClient::BindHostReceiverForRenderer(
 #endif
 }
 
-void BindBadgeManagerFrameReceiver(
-    content::RenderFrameHost* frame,
-    mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
-  LOG(WARNING) << "The Chromium Badging API is not available in Electron";
-}
-
 void BindElectronBrowser(
     content::RenderFrameHost* frame_host,
     mojo::PendingReceiver<electron::mojom::ElectronBrowser> receiver) {
@@ -1671,7 +1666,7 @@ void ElectronBrowserClient::RegisterBrowserInterfaceBindersForFrame(
   map->Add<network_hints::mojom::NetworkHintsHandler>(
       base::BindRepeating(&BindNetworkHintsHandler));
   map->Add<blink::mojom::BadgeService>(
-      base::BindRepeating(&BindBadgeManagerFrameReceiver));
+      base::BindRepeating(&badging::BadgeManager::BindFrameReceiver));
   map->Add<electron::mojom::ElectronBrowser>(
       base::BindRepeating(&BindElectronBrowser));
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)

+ 3 - 1
shell/browser/native_window_views.cc

@@ -1209,7 +1209,9 @@ void NativeWindowViews::SetProgressBar(double progress,
 void NativeWindowViews::SetOverlayIcon(const gfx::Image& overlay,
                                        const std::string& description) {
 #if defined(OS_WIN)
-  taskbar_host_.SetOverlayIcon(GetAcceleratedWidget(), overlay, description);
+  SkBitmap overlay_bitmap = overlay.AsBitmap();
+  taskbar_host_.SetOverlayIcon(GetAcceleratedWidget(), overlay_bitmap,
+                               description);
 #endif
 }
 

+ 2 - 3
shell/browser/ui/win/taskbar_host.cc

@@ -166,13 +166,12 @@ bool TaskbarHost::SetProgressBar(HWND window,
 }
 
 bool TaskbarHost::SetOverlayIcon(HWND window,
-                                 const gfx::Image& overlay,
+                                 const SkBitmap& overlay,
                                  const std::string& text) {
   if (!InitializeTaskbar())
     return false;
 
-  base::win::ScopedHICON icon(
-      IconUtil::CreateHICONFromSkBitmap(overlay.AsBitmap()));
+  base::win::ScopedHICON icon(IconUtil::CreateHICONFromSkBitmap(overlay));
   return SUCCEEDED(taskbar_->SetOverlayIcon(window, icon.get(),
                                             base::UTF8ToUTF16(text).c_str()));
 }

+ 1 - 1
shell/browser/ui/win/taskbar_host.h

@@ -48,7 +48,7 @@ class TaskbarHost {
 
   // Set the overlay icon in taskbar.
   bool SetOverlayIcon(HWND window,
-                      const gfx::Image& overlay,
+                      const SkBitmap& overlay,
                       const std::string& text);
 
   // Set the region of the window to show as a thumbnail in taskbar.

+ 12 - 16
spec-main/api-app-spec.ts

@@ -551,46 +551,42 @@ describe('app module', () => {
     const platformIsNotSupported =
         (process.platform === 'win32') ||
         (process.platform === 'linux' && !app.isUnityRunning());
-    const platformIsSupported = !platformIsNotSupported;
 
     const expectedBadgeCount = 42;
 
     after(() => { app.badgeCount = 0; });
 
-    describe('on supported platform', () => {
-      it('with properties', () => {
+    ifdescribe(!platformIsNotSupported)('on supported platform', () => {
+      describe('with properties', () => {
         it('sets a badge count', function () {
-          if (platformIsNotSupported) return this.skip();
-
           app.badgeCount = expectedBadgeCount;
           expect(app.badgeCount).to.equal(expectedBadgeCount);
         });
       });
 
-      it('with functions', () => {
-        it('sets a badge count', function () {
-          if (platformIsNotSupported) return this.skip();
-
+      describe('with functions', () => {
+        it('sets a numerical badge count', function () {
           app.setBadgeCount(expectedBadgeCount);
           expect(app.getBadgeCount()).to.equal(expectedBadgeCount);
         });
+        it('sets an non numeric (dot) badge count', function () {
+          app.setBadgeCount();
+          // Badge count should be zero when non numeric (dot) is requested
+          expect(app.getBadgeCount()).to.equal(0);
+        });
       });
     });
 
-    describe('on unsupported platform', () => {
-      it('with properties', () => {
+    ifdescribe(process.platform !== 'win32' && platformIsNotSupported)('on unsupported platform', () => {
+      describe('with properties', () => {
         it('does not set a badge count', function () {
-          if (platformIsSupported) return this.skip();
-
           app.badgeCount = 9999;
           expect(app.badgeCount).to.equal(0);
         });
       });
 
-      it('with functions', () => {
+      describe('with functions', () => {
         it('does not set a badge count)', function () {
-          if (platformIsSupported) return this.skip();
-
           app.setBadgeCount(9999);
           expect(app.getBadgeCount()).to.equal(0);
         });

+ 54 - 0
spec-main/chromium-spec.ts

@@ -1550,3 +1550,57 @@ describe('navigator.clipboard', () => {
     expect(clipboard).to.not.equal('Read permission denied.');
   });
 });
+
+ifdescribe((process.platform !== 'linux' || app.isUnityRunning()))('navigator.setAppBadge/clearAppBadge', () => {
+  let w: BrowserWindow;
+  before(async () => {
+    w = new BrowserWindow({
+      show: false
+    });
+    await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+  });
+
+  const expectedBadgeCount = 42;
+
+  const fireAppBadgeAction: any = (action: string, value: any) => {
+    return w.webContents.executeJavaScript(`
+      navigator.${action}AppBadge(${value}).then(() => 'success').catch(err => err.message)`);
+  };
+
+  // For some reason on macOS changing the badge count doesn't happen right away, so wait
+  // until it changes.
+  async function waitForBadgeCount (value: number) {
+    let badgeCount = app.getBadgeCount();
+    while (badgeCount !== value) {
+      await new Promise(resolve => setTimeout(resolve, 10));
+      badgeCount = app.getBadgeCount();
+    }
+    return badgeCount;
+  }
+
+  after(() => {
+    app.badgeCount = 0;
+    closeAllWindows();
+  });
+
+  it('setAppBadge can set a numerical value', async () => {
+    const result = await fireAppBadgeAction('set', expectedBadgeCount);
+    expect(result).to.equal('success');
+    expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
+  });
+
+  it('setAppBadge can set an empty(dot) value', async () => {
+    const result = await fireAppBadgeAction('set');
+    expect(result).to.equal('success');
+    expect(waitForBadgeCount(0)).to.eventually.equal(0);
+  });
+
+  it('clearAppBadge can clear a value', async () => {
+    let result = await fireAppBadgeAction('set', expectedBadgeCount);
+    expect(result).to.equal('success');
+    expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
+    result = await fireAppBadgeAction('clear');
+    expect(result).to.equal('success');
+    expect(waitForBadgeCount(0)).to.eventually.equal(0);
+  });
+});

+ 7 - 0
spec/chromium-spec.js

@@ -22,6 +22,13 @@ describe('chromium feature', () => {
       expect(() => {
         navigator.setAppBadge(42);
       }).to.not.throw();
+      expect(() => {
+        // setAppBadge with no argument should show dot
+        navigator.setAppBadge();
+      }).to.not.throw();
+      expect(() => {
+        navigator.clearAppBadge();
+      }).to.not.throw();
     });
   });