From 7981d955b8f84e2129f5a907445fbc0aea6b9fe8 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 13 Nov 2023 08:27:18 +0100 Subject: [PATCH] fix: `chrome.action` API registration (#40500) --- shell/browser/extensions/api/BUILD.gn | 1 + shell/common/extensions/api/BUILD.gn | 1 + .../common/extensions/api/_api_features.json | 13 + .../extensions/api/_manifest_features.json | 5 + shell/common/extensions/api/action.json | 2 +- .../common/extensions/api/browser_action.json | 370 ++++++++++++++++++ spec/extensions-spec.ts | 59 +++ .../chrome-action-fail/background.js | 28 ++ .../extensions/chrome-action-fail/main.js | 30 ++ .../chrome-action-fail/manifest.json | 19 + .../extensions/chrome-action-fail/popup.html | 9 + 11 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 shell/common/extensions/api/browser_action.json create mode 100644 spec/fixtures/extensions/chrome-action-fail/background.js create mode 100644 spec/fixtures/extensions/chrome-action-fail/main.js create mode 100644 spec/fixtures/extensions/chrome-action-fail/manifest.json create mode 100644 spec/fixtures/extensions/chrome-action-fail/popup.html 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