Browse Source

fix: `chrome.action` API registration (#40513)

* fix: chrome.action API registration

Co-authored-by: Shelley Vohr <[email protected]>

* fix: browser_action implemented_in

Co-authored-by: Shelley Vohr <[email protected]>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <[email protected]>
trop[bot] 1 year ago
parent
commit
8472c6a3d7

+ 1 - 0
shell/browser/extensions/api/BUILD.gn

@@ -11,6 +11,7 @@ assert(enable_extensions,
 
 function_registration("api_registration") {
   sources = [
+    "//electron/shell/common/extensions/api/action.json",
     "//electron/shell/common/extensions/api/extension.json",
     "//electron/shell/common/extensions/api/resources_private.idl",
     "//electron/shell/common/extensions/api/scripting.idl",

+ 1 - 0
shell/common/extensions/api/BUILD.gn

@@ -38,6 +38,7 @@ group("extensions_features") {
 generated_json_strings("generated_api_json_strings") {
   sources = [
     "action.json",
+    "browser_action.json",
     "extension.json",
     "resources_private.idl",
     "scripting.idl",

+ 13 - 0
shell/common/extensions/api/_api_features.json

@@ -1,4 +1,17 @@
 {
+  "action": {
+    "dependencies": ["manifest:action"],
+    "contexts": ["blessed_extension"]
+  },
+  "action.isEnabled": {
+    "channel": "stable"
+  },
+  "action.getBadgeTextColor": {
+    "channel": "stable"
+  },
+  "action.setBadgeTextColor": {
+    "channel": "stable"
+  },
   "tabs": {
     "channel": "stable",
     "extension_types": ["extension"],

+ 5 - 0
shell/common/extensions/api/_manifest_features.json

@@ -7,6 +7,11 @@
 // well as feature.h, simple_feature.h, and feature_provider.h.
 
 {
+  "action": {
+    "channel": "stable",
+    "extension_types": ["extension"],
+    "min_manifest_version": 3
+  },
   "author": {
     "channel": "stable",
     "extension_types": "all"

+ 1 - 1
shell/common/extensions/api/action.json

@@ -6,7 +6,7 @@
     "namespace": "action",
     "description": "Use the <code>chrome.action</code> API to control the extension's icon in the Google Chrome toolbar.",
     "compiler_options": {
-      "implemented_in": "shell/browser/extensions/api/extension_action/extension_action_api.h"
+      "implemented_in": "electron/shell/browser/extensions/api/extension_action/extension_action_api.h"
     },
     "types": [
       {

+ 370 - 0
shell/common/extensions/api/browser_action.json

@@ -0,0 +1,370 @@
+// Copyright 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+[
+  {
+    "namespace": "browserAction",
+    "description": "Use browser actions to put icons in the main Google Chrome toolbar, to the right of the address bar. In addition to its <a href='browserAction#icon'>icon</a>, a browser action can have a <a href='browserAction#tooltip'>tooltip</a>, a <a href='browserAction#badge'>badge</a>, and a <a href='browserAction#popups'>popup</a>.",
+    "compiler_options": {
+      "implemented_in": "electron/shell/browser/extensions/api/extension_action/extension_action_api.h"
+    },
+    "types": [
+      {
+        "id": "ColorArray",
+        "type": "array",
+        "items": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 255
+        },
+        "minItems": 4,
+        "maxItems": 4
+      },
+      {
+        "id": "ImageDataType",
+        "type": "object",
+        "isInstanceOf": "ImageData",
+        "additionalProperties": {
+          "type": "any"
+        },
+        "description": "Pixel data for an image. Must be an ImageData object; for example, from a <code>canvas</code> element."
+      },
+      {
+        "id": "TabDetails",
+        "type": "object",
+        "properties": {
+          "tabId": {
+            "type": "integer",
+            "optional": true,
+            "minimum": 0,
+            "description": "The ID of the tab to query state for. If no tab is specified, the non-tab-specific state is returned."
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "setTitle",
+        "type": "function",
+        "description": "Sets the title of the browser action. This title appears in the tooltip.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "title": {
+                "type": "string",
+                "description": "The string the browser action should display when moused over."
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "getTitle",
+        "type": "function",
+        "description": "Gets the title of the browser action.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "TabDetails"
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [
+            {
+              "name": "result",
+              "type": "string"
+            }
+          ]
+        }
+      },
+      {
+        "name": "setIcon",
+        "type": "function",
+        "description": "Sets the icon for the browser action. The icon can be specified as the path to an image file, as the pixel data from a canvas element, or as a dictionary of one of those. Either the <code>path</code> or the <code>imageData</code> property must be specified.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "imageData": {
+                "choices": [
+                  {
+                    "$ref": "ImageDataType"
+                  },
+                  {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "any"
+                    }
+                  }
+                ],
+                "optional": true,
+                "description": "Either an ImageData object or a dictionary {size -> ImageData} representing an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then an image with size <code>scale</code> * n is selected, where <i>n</i> is the size of the icon in the UI. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'16': foo}'"
+              },
+              "path": {
+                "choices": [
+                  {
+                    "type": "string"
+                  },
+                  {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "any"
+                    }
+                  }
+                ],
+                "optional": true,
+                "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to an icon to be set. If the icon is specified as a dictionary, the image used is chosen depending on the screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then an image with size <code>scale</code> * n is selected, where <i>n</i> is the size of the icon in the UI. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.path = {'16': foo}'"
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "optional": true,
+          "parameters": []
+        }
+      },
+      {
+        "name": "setPopup",
+        "type": "function",
+        "description": "Sets the HTML document to be opened as a popup when the user clicks the browser action icon.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": 0,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              },
+              "popup": {
+                "type": "string",
+                "description": "The relative path to the HTML file to show in a popup. If set to the empty string (<code>''</code>), no popup is shown."
+              }
+            }
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "getPopup",
+        "type": "function",
+        "description": "Gets the HTML document that is set as the popup for this browser action.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "TabDetails"
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [
+            {
+              "name": "result",
+              "type": "string"
+            }
+          ]
+        }
+      },
+      {
+        "name": "setBadgeText",
+        "type": "function",
+        "description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "text": {
+                "type": "string",
+                "optional": true,
+                "description": "Any number of characters can be passed, but only about four can fit into the space. If an empty string (<code>''</code>) is passed, the badge text is cleared.  If <code>tabId</code> is specified and <code>text</code> is null, the text for the specified tab is cleared and defaults to the global badge text."
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "getBadgeText",
+        "type": "function",
+        "description": "Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "TabDetails"
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [
+            {
+              "name": "result",
+              "type": "string"
+            }
+          ]
+        }
+      },
+      {
+        "name": "setBadgeBackgroundColor",
+        "type": "function",
+        "description": "Sets the background color for the badge.",
+        "parameters": [
+          {
+            "name": "details",
+            "type": "object",
+            "properties": {
+              "color": {
+                "description": "An array of four integers in the range 0-255 that make up the RGBA color of the badge. Can also be a string with a CSS hex color value; for example, <code>#FF0000</code> or <code>#F00</code> (red). Renders colors at full opacity.",
+                "choices": [
+                  {
+                    "type": "string"
+                  },
+                  {
+                    "$ref": "ColorArray"
+                  }
+                ]
+              },
+              "tabId": {
+                "type": "integer",
+                "optional": true,
+                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
+              }
+            }
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "getBadgeBackgroundColor",
+        "type": "function",
+        "description": "Gets the background color of the browser action.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "TabDetails"
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [
+            {
+              "name": "result",
+              "$ref": "ColorArray"
+            }
+          ]
+        }
+      },
+      {
+        "name": "enable",
+        "type": "function",
+        "description": "Enables the browser action for a tab. Defaults to enabled.",
+        "parameters": [
+          {
+            "type": "integer",
+            "optional": true,
+            "name": "tabId",
+            "minimum": 0,
+            "description": "The ID of the tab for which to modify the browser action."
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "disable",
+        "type": "function",
+        "description": "Disables the browser action for a tab.",
+        "parameters": [
+          {
+            "type": "integer",
+            "optional": true,
+            "name": "tabId",
+            "minimum": 0,
+            "description": "The ID of the tab for which to modify the browser action."
+          }
+        ],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [],
+          "optional": true
+        }
+      },
+      {
+        "name": "openPopup",
+        "type": "function",
+        "description": "Opens the extension popup window in the active window but does not grant tab permissions.",
+        "nodoc": true,
+        "parameters": [],
+        "returns_async": {
+          "name": "callback",
+          "parameters": [
+            {
+              "name": "popupView",
+              "type": "object",
+              "optional": true,
+              "description": "JavaScript 'window' object for the popup window if it was succesfully opened.",
+              "additionalProperties": {
+                "type": "any"
+              }
+            }
+          ]
+        }
+      }
+    ],
+    "events": [
+      {
+        "name": "onClicked",
+        "type": "function",
+        "description": "Fired when a browser action icon is clicked. Does not fire if the browser action has a popup.",
+        "parameters": [
+          {
+            "name": "tab",
+            "$ref": "tabs.Tab"
+          }
+        ]
+      }
+    ]
+  }
+]

+ 59 - 0
spec/extensions-spec.ts

@@ -897,6 +897,65 @@ describe('chrome extensions', () => {
       });
     });
 
+    // chrome.action is not supported in Electron. These tests only ensure
+    // it does not explode.
+    describe('chrome.action', () => {
+      let customSession: Session;
+      let w = null as unknown as BrowserWindow;
+
+      before(async () => {
+        customSession = session.fromPartition(`persist:${uuid.v4()}`);
+        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
+      });
+
+      beforeEach(() => {
+        w = new BrowserWindow({
+          show: false,
+          webPreferences: {
+            session: customSession
+          }
+        });
+      });
+
+      afterEach(closeAllWindows);
+
+      it('isEnabled', async () => {
+        await w.loadURL(url);
+
+        const message = { method: 'isEnabled' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const [, , responseString] = await once(w.webContents, 'console-message');
+
+        const response = JSON.parse(responseString);
+        expect(response).to.equal(false);
+      });
+
+      it('setIcon', async () => {
+        await w.loadURL(url);
+
+        const message = { method: 'setIcon' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const [, , responseString] = await once(w.webContents, 'console-message');
+
+        const response = JSON.parse(responseString);
+        expect(response).to.equal(null);
+      });
+
+      it('getBadgeText', async () => {
+        await w.loadURL(url);
+
+        const message = { method: 'getBadgeText' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const [, , responseString] = await once(w.webContents, 'console-message');
+
+        const response = JSON.parse(responseString);
+        expect(response).to.equal('');
+      });
+    });
+
     describe('chrome.tabs', () => {
       let customSession: Session;
       let w = null as unknown as BrowserWindow;

+ 28 - 0
spec/fixtures/extensions/chrome-action-fail/background.js

@@ -0,0 +1,28 @@
+/* global chrome */
+
+const handleRequest = async (request, sender, sendResponse) => {
+  const { method } = request;
+  const tabId = sender.tab.id;
+
+  switch (method) {
+    case 'isEnabled': {
+      chrome.action.isEnabled(tabId).then(sendResponse);
+      break;
+    }
+
+    case 'setIcon': {
+      chrome.action.setIcon({ tabId, imageData: {} }).then(sendResponse);
+      break;
+    }
+
+    case 'getBadgeText': {
+      chrome.action.getBadgeText({ tabId }).then(sendResponse);
+      break;
+    }
+  }
+};
+
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+  handleRequest(request, sender, sendResponse);
+  return true;
+});

+ 30 - 0
spec/fixtures/extensions/chrome-action-fail/main.js

@@ -0,0 +1,30 @@
+/* global chrome */
+
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+  sendResponse(request);
+});
+
+const testMap = {
+  isEnabled () {
+    chrome.runtime.sendMessage({ method: 'isEnabled' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  },
+  setIcon () {
+    chrome.runtime.sendMessage({ method: 'setIcon' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  },
+  getBadgeText () {
+    chrome.runtime.sendMessage({ method: 'getBadgeText' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  }
+};
+
+const dispatchTest = (event) => {
+  const { method, args = [] } = JSON.parse(event.data);
+  testMap[method](...args);
+};
+
+window.addEventListener('message', dispatchTest, false);

+ 19 - 0
spec/fixtures/extensions/chrome-action-fail/manifest.json

@@ -0,0 +1,19 @@
+{
+  "name": "Action popup demo",
+  "version": "1.0",
+  "manifest_version": 3,
+  "background": {
+    "service_worker": "background.js"
+  },
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "js": ["main.js"],
+      "run_at": "document_start"
+    }
+  ],
+  "action": {
+    "default_title": "Click Me",
+    "default_popup": "popup.html"
+  }
+}

+ 9 - 0
spec/fixtures/extensions/chrome-action-fail/popup.html

@@ -0,0 +1,9 @@
+<html>
+
+<body>
+  <script type="text/javascript" charset="utf-8">
+    console.log('b');
+  </script>
+</body>
+
+</html>