diff --git a/shell/browser/extensions/api/BUILD.gn b/shell/browser/extensions/api/BUILD.gn
index 5e10759b9335..9be5160521bc 100644
--- a/shell/browser/extensions/api/BUILD.gn
+++ b/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",
diff --git a/shell/common/extensions/api/BUILD.gn b/shell/common/extensions/api/BUILD.gn
index e40defe1a8ee..da96283d4a81 100644
--- a/shell/common/extensions/api/BUILD.gn
+++ b/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",
diff --git a/shell/common/extensions/api/_api_features.json b/shell/common/extensions/api/_api_features.json
index 8a070b338b45..1358a507e1b0 100644
--- a/shell/common/extensions/api/_api_features.json
+++ b/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"],
diff --git a/shell/common/extensions/api/_manifest_features.json b/shell/common/extensions/api/_manifest_features.json
index 248fe3df2be4..1df3e863f290 100644
--- a/shell/common/extensions/api/_manifest_features.json
+++ b/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"
diff --git a/shell/common/extensions/api/action.json b/shell/common/extensions/api/action.json
index 0c109ba94de8..94dcea6cd4d4 100644
--- a/shell/common/extensions/api/action.json
+++ b/shell/common/extensions/api/action.json
@@ -6,7 +6,7 @@
"namespace": "action",
"description": "Use the chrome.action
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": [
{
diff --git a/shell/common/extensions/api/browser_action.json b/shell/common/extensions/api/browser_action.json
new file mode 100644
index 000000000000..1b72d5726979
--- /dev/null
+++ b/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 icon, a browser action can have a tooltip, a badge, and a popup.",
+ "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 canvas
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 path
or the imageData
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 scale
, then an image with size scale
* n is selected, where n 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 scale
, then an image with size scale
* n is selected, where n 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 (''
), 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 (''
) is passed, the badge text is cleared. If tabId
is specified and text
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, #FF0000
or #F00
(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"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/spec/extensions-spec.ts b/spec/extensions-spec.ts
index cec35a996994..e160581f58f5 100644
--- a/spec/extensions-spec.ts
+++ b/spec/extensions-spec.ts
@@ -898,6 +898,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;
diff --git a/spec/fixtures/extensions/chrome-action-fail/background.js b/spec/fixtures/extensions/chrome-action-fail/background.js
new file mode 100644
index 000000000000..d662e230555c
--- /dev/null
+++ b/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;
+});
diff --git a/spec/fixtures/extensions/chrome-action-fail/main.js b/spec/fixtures/extensions/chrome-action-fail/main.js
new file mode 100644
index 000000000000..9c97da1dc34e
--- /dev/null
+++ b/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);
diff --git a/spec/fixtures/extensions/chrome-action-fail/manifest.json b/spec/fixtures/extensions/chrome-action-fail/manifest.json
new file mode 100644
index 000000000000..d1041d47ac80
--- /dev/null
+++ b/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": [""],
+ "js": ["main.js"],
+ "run_at": "document_start"
+ }
+ ],
+ "action": {
+ "default_title": "Click Me",
+ "default_popup": "popup.html"
+ }
+}
\ No newline at end of file
diff --git a/spec/fixtures/extensions/chrome-action-fail/popup.html b/spec/fixtures/extensions/chrome-action-fail/popup.html
new file mode 100644
index 000000000000..d5865f3c0cff
--- /dev/null
+++ b/spec/fixtures/extensions/chrome-action-fail/popup.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file