mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
Compare commits
31 Commits
v31.0.0
...
v27.0.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe0a90341 | ||
|
|
f3dcd3b287 | ||
|
|
05de1d7906 | ||
|
|
c14f5369af | ||
|
|
3901b38e01 | ||
|
|
e14b09c6c5 | ||
|
|
e4ecade9cb | ||
|
|
04fad161e0 | ||
|
|
89f8c0efed | ||
|
|
48c537e51e | ||
|
|
3d3852343c | ||
|
|
d51a1e4c23 | ||
|
|
43d0dcfc27 | ||
|
|
056c343c04 | ||
|
|
5707be53af | ||
|
|
e1465f6723 | ||
|
|
c406384e8c | ||
|
|
c5ce6de82b | ||
|
|
d89b7f0a4e | ||
|
|
dfbd4c4335 | ||
|
|
d79189056d | ||
|
|
6fd069231f | ||
|
|
56e749782e | ||
|
|
864dd4af40 | ||
|
|
3d31570f8d | ||
|
|
3411959886 | ||
|
|
b0b1f2c727 | ||
|
|
4128e9f0e0 | ||
|
|
0bd5e36411 | ||
|
|
c97a4ce691 | ||
|
|
fbb982350b |
@@ -249,19 +249,19 @@ step-depot-tools-get: &step-depot-tools-get
|
||||
cd depot_tools
|
||||
cat > gclient.diff \<< 'EOF'
|
||||
diff --git a/gclient.py b/gclient.py
|
||||
index 3a9c5c6..f222043 100755
|
||||
index c305c248..e6e0fbdc 100755
|
||||
--- a/gclient.py
|
||||
+++ b/gclient.py
|
||||
@@ -712,7 +712,8 @@ class Dependency(gclient_utils.WorkItem, DependencySettings):
|
||||
|
||||
if dep_type == 'cipd':
|
||||
cipd_root = self.GetCipdRoot()
|
||||
- for package in dep_value.get('packages', []):
|
||||
+ packages = dep_value.get('packages', [])
|
||||
+ for package in (x for x in packages if "infra/3pp/tools/swift-format" not in x.get('package')):
|
||||
deps_to_add.append(
|
||||
CipdDependency(
|
||||
parent=self,
|
||||
@@ -735,7 +735,8 @@ class Dependency(gclient_utils.WorkItem, DependencySettings):
|
||||
|
||||
if dep_type == 'cipd':
|
||||
cipd_root = self.GetCipdRoot()
|
||||
- for package in dep_value.get('packages', []):
|
||||
+ packages = dep_value.get('packages', [])
|
||||
+ for package in (x for x in packages if "infra/3pp/tools/swift-format" not in x.get('package')):
|
||||
deps_to_add.append(
|
||||
CipdDependency(parent=self,
|
||||
name=name,
|
||||
EOF
|
||||
git apply --3way gclient.diff
|
||||
fi
|
||||
|
||||
@@ -38,7 +38,7 @@ For more installation options and troubleshooting tips, see
|
||||
|
||||
Each Electron release provides binaries for macOS, Windows, and Linux.
|
||||
|
||||
* macOS (High Sierra and up): Electron provides 64-bit Intel and ARM binaries for macOS. Apple Silicon support was added in Electron 11.
|
||||
* macOS (Catalina and up): Electron provides 64-bit Intel and ARM binaries for macOS. Apple Silicon support was added in Electron 11.
|
||||
* Windows (Windows 10 and up): Electron provides `ia32` (`x86`), `x64` (`amd64`), and `arm64` binaries for Windows. Windows on ARM support was added in Electron 5.0.8. Support for Windows 7, 8 and 8.1 was [removed in Electron 23, in line with Chromium's Windows deprecation policy](https://www.electronjs.org/blog/windows-7-to-8-1-deprecation-notice).
|
||||
* Linux: The prebuilt binaries of Electron are built on Ubuntu 20.04. They have also been verified to work on:
|
||||
* Ubuntu 14.04 and newer
|
||||
@@ -78,7 +78,7 @@ binary. Use this to spawn Electron from Node scripts:
|
||||
|
||||
```javascript
|
||||
const electron = require('electron')
|
||||
const proc = require('child_process')
|
||||
const proc = require('node:child_process')
|
||||
|
||||
// will print something similar to /Users/maf/.../Electron
|
||||
console.log(electron)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
version: 1.0.{build}
|
||||
build_cloud: electronhq-16-core
|
||||
image: e-118.0.5949.0
|
||||
image: e-118.0.5991.0
|
||||
environment:
|
||||
GIT_CACHE_PATH: C:\Users\appveyor\libcc_cache
|
||||
ELECTRON_OUT_DIR: Default
|
||||
@@ -118,7 +118,7 @@ for:
|
||||
- ps: .\src\electron\script\start-goma.ps1 -gomaDir $env:LOCAL_GOMA_DIR
|
||||
- ps: >-
|
||||
if (Test-Path 'env:RAW_GOMA_AUTH') {
|
||||
$goma_login = python $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
$goma_login = python3 $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
if ($goma_login -eq 'Login as Fermi Planck') {
|
||||
Write-warning "Goma authentication is correct";
|
||||
} else {
|
||||
@@ -168,7 +168,7 @@ for:
|
||||
- ninja -C out/Default electron:hunspell_dictionaries_zip
|
||||
- ninja -C out/Default electron:electron_chromedriver_zip
|
||||
- ninja -C out/Default third_party/electron_node:headers
|
||||
- python %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- python3 %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- ps: >-
|
||||
Get-CimInstance -Namespace root\cimv2 -Class Win32_product | Select vendor, description, @{l='install_location';e='InstallLocation'}, @{l='install_date';e='InstallDate'}, @{l='install_date_2';e='InstallDate2'}, caption, version, name, @{l='sku_number';e='SKUNumber'} | ConvertTo-Json | Out-File -Encoding utf8 -FilePath .\installed_software.json
|
||||
- python3 electron/build/profile_toolchain.py --output-json=out/Default/windows_toolchain_profile.json
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
version: 1.0.{build}
|
||||
build_cloud: electronhq-16-core
|
||||
image: e-118.0.5949.0
|
||||
image: e-118.0.5991.0
|
||||
environment:
|
||||
GIT_CACHE_PATH: C:\Users\appveyor\libcc_cache
|
||||
ELECTRON_OUT_DIR: Default
|
||||
@@ -116,7 +116,7 @@ for:
|
||||
- ps: .\src\electron\script\start-goma.ps1 -gomaDir $env:LOCAL_GOMA_DIR
|
||||
- ps: >-
|
||||
if (Test-Path 'env:RAW_GOMA_AUTH') {
|
||||
$goma_login = python $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
$goma_login = python3 $env:LOCAL_GOMA_DIR\goma_auth.py info
|
||||
if ($goma_login -eq 'Login as Fermi Planck') {
|
||||
Write-warning "Goma authentication is correct";
|
||||
} else {
|
||||
@@ -166,7 +166,7 @@ for:
|
||||
- ninja -C out/Default electron:hunspell_dictionaries_zip
|
||||
- ninja -C out/Default electron:electron_chromedriver_zip
|
||||
- ninja -C out/Default third_party/electron_node:headers
|
||||
- python %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- python3 %LOCAL_GOMA_DIR%\goma_ctl.py stat
|
||||
- ps: >-
|
||||
Get-CimInstance -Namespace root\cimv2 -Class Win32_product | Select vendor, description, @{l='install_location';e='InstallLocation'}, @{l='install_date';e='InstallDate'}, @{l='install_date_2';e='InstallDate2'}, caption, version, name, @{l='sku_number';e='SKUNumber'} | ConvertTo-Json | Out-File -Encoding utf8 -FilePath .\installed_software.json
|
||||
- python3 electron/build/profile_toolchain.py --output-json=out/Default/windows_toolchain_profile.json
|
||||
|
||||
@@ -412,18 +412,7 @@ Returns:
|
||||
|
||||
* `event` Event
|
||||
* `webContents` [WebContents](web-contents.md)
|
||||
* `details` Object
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Emitted when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
@@ -1344,7 +1333,7 @@ application name. For example:
|
||||
|
||||
``` js
|
||||
const { app } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const appFolder = path.dirname(process.execPath)
|
||||
const updateExe = path.resolve(appFolder, '..', 'Update.exe')
|
||||
@@ -1415,7 +1404,7 @@ Returns `Function` - This function **must** be called once you have finished acc
|
||||
|
||||
```js
|
||||
const { app, dialog } = require('electron')
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
|
||||
let filepath
|
||||
let bookmark
|
||||
|
||||
@@ -820,10 +820,14 @@ win.setBounds({ width: 100 })
|
||||
console.log(win.getBounds())
|
||||
```
|
||||
|
||||
**Note:** On macOS, the y-coordinate value cannot be smaller than the [Tray](tray.md) height. The tray height has changed over time and depends on the operating system, but is between 20-40px. Passing a value lower than the tray height will result in a window that is flush to the tray.
|
||||
|
||||
#### `win.getBounds()`
|
||||
|
||||
Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `Object`.
|
||||
|
||||
**Note:** On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
|
||||
|
||||
#### `win.getBackgroundColor()`
|
||||
|
||||
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.
|
||||
@@ -1207,7 +1211,7 @@ const win = new BrowserWindow()
|
||||
const url = require('url').format({
|
||||
protocol: 'file',
|
||||
slashes: true,
|
||||
pathname: require('path').join(__dirname, 'index.html')
|
||||
pathname: require('node:path').join(__dirname, 'index.html')
|
||||
})
|
||||
|
||||
win.loadURL(url)
|
||||
|
||||
@@ -147,7 +147,7 @@ Be very cautious about which globals and APIs you expose to untrusted remote con
|
||||
|
||||
```javascript
|
||||
const { contextBridge } = require('electron')
|
||||
const crypto = require('crypto')
|
||||
const crypto = require('node:crypto')
|
||||
contextBridge.exposeInMainWorld('nodeCrypto', {
|
||||
sha256sum (data) {
|
||||
const hash = crypto.createHash('sha256')
|
||||
|
||||
@@ -40,18 +40,41 @@ We support the following extensions APIs, with some caveats. Other APIs may
|
||||
additionally be supported, but support for any APIs not listed here is
|
||||
provisional and may be removed.
|
||||
|
||||
### Supported Manifest Keys
|
||||
|
||||
- `name`
|
||||
- `version`
|
||||
- `author`
|
||||
- `permissions`
|
||||
- `content_scripts`
|
||||
- `default_locale`
|
||||
- `devtools_page`
|
||||
- `short_name`
|
||||
- `host_permissions` (Manifest V3)
|
||||
- `manifest_version`
|
||||
- `background` (Manifest V2)
|
||||
- `minimum_chrome_version`
|
||||
|
||||
See [Manifest file format](https://developer.chrome.com/docs/extensions/mv3/manifest/) for more information about the purpose of each possible key.
|
||||
|
||||
### `chrome.devtools.inspectedWindow`
|
||||
|
||||
All features of this API are supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_inspectedWindow) for more information.
|
||||
|
||||
### `chrome.devtools.network`
|
||||
|
||||
All features of this API are supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_network) for more information.
|
||||
|
||||
### `chrome.devtools.panels`
|
||||
|
||||
All features of this API are supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_panels) for more information.
|
||||
|
||||
### `chrome.extension`
|
||||
|
||||
The following properties of `chrome.extension` are supported:
|
||||
@@ -63,6 +86,25 @@ The following methods of `chrome.extension` are supported:
|
||||
- `chrome.extension.getURL`
|
||||
- `chrome.extension.getBackgroundPage`
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/extension) for more information.
|
||||
|
||||
### `chrome.management`
|
||||
|
||||
The following methods of `chrome.management` are supported:
|
||||
|
||||
- `chrome.management.getAll`
|
||||
- `chrome.management.get`
|
||||
- `chrome.management.getSelf`
|
||||
- `chrome.management.getPermissionWarningsById`
|
||||
- `chrome.management.getPermissionWarningsByManifest`
|
||||
|
||||
The following events of `chrome.management` are supported:
|
||||
|
||||
- `chrome.management.onEnabled`
|
||||
- `chrome.management.onDisabled`
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/management) for more information.
|
||||
|
||||
### `chrome.runtime`
|
||||
|
||||
The following properties of `chrome.runtime` are supported:
|
||||
@@ -89,12 +131,24 @@ The following events of `chrome.runtime` are supported:
|
||||
- `chrome.runtime.onConnect`
|
||||
- `chrome.runtime.onMessage`
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/runtime) for more information.
|
||||
|
||||
### `chrome.scripting`
|
||||
|
||||
All features of this API are supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/scripting) for more information.
|
||||
|
||||
### `chrome.storage`
|
||||
|
||||
The following methods of `chrome.storage` are supported:
|
||||
|
||||
- `chrome.storage.local`
|
||||
|
||||
`chrome.storage.sync` and `chrome.storage.managed` are **not** supported.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/storage) for more information.
|
||||
|
||||
### `chrome.tabs`
|
||||
|
||||
The following methods of `chrome.tabs` are supported:
|
||||
@@ -111,23 +165,12 @@ The following methods of `chrome.tabs` are supported:
|
||||
> tab". Since Electron has no such concept, passing `-1` as a tab ID is not
|
||||
> supported and will raise an error.
|
||||
|
||||
### `chrome.management`
|
||||
|
||||
The following methods of `chrome.management` are supported:
|
||||
|
||||
- `chrome.management.getAll`
|
||||
- `chrome.management.get`
|
||||
- `chrome.management.getSelf`
|
||||
- `chrome.management.getPermissionWarningsById`
|
||||
- `chrome.management.getPermissionWarningsByManifest`
|
||||
|
||||
The following events of `chrome.management` are supported:
|
||||
|
||||
- `chrome.management.onEnabled`
|
||||
- `chrome.management.onDisabled`
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/tabs) for more information.
|
||||
|
||||
### `chrome.webRequest`
|
||||
|
||||
All features of this API are supported.
|
||||
|
||||
> **NOTE:** Electron's [`webRequest`](web-request.md) module takes precedence over `chrome.webRequest` if there are conflicting handlers.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/webRequest) for more information.
|
||||
|
||||
@@ -33,7 +33,7 @@ to register it to that session explicitly.
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow, net, protocol, session } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
const url = require('url')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
@@ -122,7 +122,7 @@ Example:
|
||||
|
||||
```js
|
||||
const { app, net, protocol } = require('electron')
|
||||
const { join } = require('path')
|
||||
const { join } = require('node:path')
|
||||
const { pathToFileURL } = require('url')
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
|
||||
@@ -103,7 +103,7 @@ const { session } = require('electron')
|
||||
session.defaultSession.on('will-download', (event, item, webContents) => {
|
||||
event.preventDefault()
|
||||
require('got')(item.getURL()).then((response) => {
|
||||
require('fs').writeFileSync('/somewhere', response.body)
|
||||
require('node:fs').writeFileSync('/somewhere', response.body)
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -1193,7 +1193,7 @@ automatically. To clear the handler, call `setBluetoothPairingHandler(null)`.
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow, session } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
function createWindow () {
|
||||
let bluetoothPinCallback = null
|
||||
@@ -1311,7 +1311,7 @@ The API will generate a [DownloadItem](download-item.md) that can be accessed
|
||||
with the [will-download](#event-will-download) event.
|
||||
|
||||
**Note:** This does not perform any security checks that relate to a page's origin,
|
||||
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl).
|
||||
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl-options).
|
||||
|
||||
#### `ses.createInterruptedDownload(options)`
|
||||
|
||||
@@ -1457,7 +1457,7 @@ extension to be loaded.
|
||||
|
||||
```js
|
||||
const { app, session } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
await session.defaultSession.loadExtension(
|
||||
@@ -1491,7 +1491,7 @@ is emitted.
|
||||
|
||||
* `extensionId` string - ID of extension to query
|
||||
|
||||
Returns `Extension` | `null` - The loaded extension with the given ID.
|
||||
Returns `Extension | null` - The loaded extension with the given ID.
|
||||
|
||||
**Note:** This API cannot be called before the `ready` event of the `app` module
|
||||
is emitted.
|
||||
@@ -1544,7 +1544,7 @@ A [`Protocol`](protocol.md) object for this session.
|
||||
|
||||
```javascript
|
||||
const { app, session } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const protocol = session.fromPartition('some-partition').protocol
|
||||
|
||||
13
docs/api/structures/render-process-gone-details.md
Normal file
13
docs/api/structures/render-process-gone-details.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# RenderProcessGoneDetails Object
|
||||
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
@@ -479,18 +479,7 @@ checking `reason === 'killed'` when you switch to that event.
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `reason` string - The reason the render process is gone. Possible values:
|
||||
* `clean-exit` - Process exited with an exit code of zero
|
||||
* `abnormal-exit` - Process exited with a non-zero exit code
|
||||
* `killed` - Process was sent a SIGTERM or otherwise killed externally
|
||||
* `crashed` - Process crashed
|
||||
* `oom` - Process ran out of memory
|
||||
* `launch-failed` - Process never successfully launched
|
||||
* `integrity-failure` - Windows code integrity checks failed
|
||||
* `exitCode` Integer - The exit code of the process, unless `reason` is
|
||||
`launch-failed`, in which case `exitCode` will be a platform-specific
|
||||
launch failure error code.
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Emitted when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
@@ -1057,9 +1046,11 @@ const win = new BrowserWindow()
|
||||
win.loadFile('src/index.html')
|
||||
```
|
||||
|
||||
#### `contents.downloadURL(url)`
|
||||
#### `contents.downloadURL(url[, options])`
|
||||
|
||||
* `url` string
|
||||
* `options` Object (optional)
|
||||
* `headers` Record<string, string> (optional) - HTTP request headers.
|
||||
|
||||
Initiates a download of the resource at `url` without navigating. The
|
||||
`will-download` event of `session` will be triggered.
|
||||
@@ -1545,14 +1536,6 @@ If you would like the page to stay hidden, you should ensure that `stayHidden` i
|
||||
Returns `boolean` - Whether this page is being captured. It returns true when the capturer count
|
||||
is large then 0.
|
||||
|
||||
#### `contents.getPrinters()` _Deprecated_
|
||||
|
||||
Get the system printer list.
|
||||
|
||||
Returns [`PrinterInfo[]`](structures/printer-info.md)
|
||||
|
||||
**Deprecated:** Should use the new [`contents.getPrintersAsync`](web-contents.md#contentsgetprintersasync) API.
|
||||
|
||||
#### `contents.getPrintersAsync()`
|
||||
|
||||
Get the system printer list.
|
||||
@@ -1646,9 +1629,9 @@ An example of `webContents.printToPDF`:
|
||||
|
||||
```javascript
|
||||
const { BrowserWindow } = require('electron')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const os = require('node:os')
|
||||
|
||||
const win = new BrowserWindow()
|
||||
win.loadURL('https://github.com')
|
||||
|
||||
@@ -280,9 +280,11 @@ if the page fails to load (see
|
||||
Loads the `url` in the webview, the `url` must contain the protocol prefix,
|
||||
e.g. the `http://` or `file://`.
|
||||
|
||||
### `<webview>.downloadURL(url)`
|
||||
### `<webview>.downloadURL(url[, options])`
|
||||
|
||||
* `url` string
|
||||
* `options` Object (optional)
|
||||
* `headers` Record<string, string> (optional) - HTTP request headers.
|
||||
|
||||
Initiates a download of the resource at `url` without navigating.
|
||||
|
||||
@@ -983,9 +985,22 @@ ipcRenderer.on('ping', () => {
|
||||
})
|
||||
```
|
||||
|
||||
### Event: 'crashed'
|
||||
### Event: 'crashed' _Deprecated_
|
||||
|
||||
Fired when the renderer process is crashed.
|
||||
Fired when the renderer process crashes or is killed.
|
||||
|
||||
**Deprecated:** This event is superceded by the `render-process-gone` event
|
||||
which contains more information about why the render process disappeared. It
|
||||
isn't always because it crashed.
|
||||
|
||||
### Event: 'render-process-gone'
|
||||
|
||||
Returns:
|
||||
|
||||
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
|
||||
|
||||
Fired when the renderer process unexpectedly disappears. This is normally
|
||||
because it was crashed or killed.
|
||||
|
||||
### Event: 'plugin-crashed'
|
||||
|
||||
|
||||
@@ -45,6 +45,22 @@ systemPreferences.on('high-contrast-color-scheme-changed', () => { /* ... */ })
|
||||
nativeTheme.on('updated', () => { /* ... */ })
|
||||
```
|
||||
|
||||
### Removed: `webContents.getPrinters`
|
||||
|
||||
The `webContents.getPrinters` method has been removed. Use
|
||||
`webContents.getPrintersAsync` instead.
|
||||
|
||||
```js
|
||||
const w = new BrowserWindow({ show: false })
|
||||
|
||||
// Removed
|
||||
console.log(w.webContents.getPrinters())
|
||||
// Replace with
|
||||
w.webContents.getPrintersAsync().then((printers) => {
|
||||
console.log(printers)
|
||||
})
|
||||
```
|
||||
|
||||
## Planned Breaking API Changes (26.0)
|
||||
|
||||
### Deprecated: `webContents.getPrinters`
|
||||
|
||||
@@ -42,14 +42,14 @@ $ asar list /path/to/example.asar
|
||||
Read a file in the ASAR archive:
|
||||
|
||||
```javascript
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
fs.readFileSync('/path/to/example.asar/file.txt')
|
||||
```
|
||||
|
||||
List all files under the root of the archive:
|
||||
|
||||
```javascript
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
fs.readdirSync('/path/to/example.asar')
|
||||
```
|
||||
|
||||
@@ -99,7 +99,7 @@ You can also set `process.noAsar` to `true` to disable the support for `asar` in
|
||||
the `fs` module:
|
||||
|
||||
```javascript
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
process.noAsar = true
|
||||
fs.readFileSync('/path/to/example.asar')
|
||||
```
|
||||
|
||||
@@ -260,7 +260,7 @@ To create a custom driver, we'll use Node.js' [`child_process`](https://nodejs.o
|
||||
The test suite will spawn the Electron process, then establish a simple messaging protocol:
|
||||
|
||||
```js title='testDriver.js' @ts-nocheck
|
||||
const childProcess = require('child_process')
|
||||
const childProcess = require('node:child_process')
|
||||
const electronPath = require('electron')
|
||||
|
||||
// spawn the process
|
||||
|
||||
@@ -136,7 +136,7 @@ Finally, the `main.js` file represents the main process and contains the actual
|
||||
|
||||
```js
|
||||
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const createWindow = () => {
|
||||
const win = new BrowserWindow({
|
||||
|
||||
@@ -35,8 +35,8 @@ Using the [React Developer Tools][react-devtools] as an example:
|
||||
|
||||
```javascript
|
||||
const { app, session } = require('electron')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const path = require('node:path')
|
||||
const os = require('node:os')
|
||||
|
||||
// on macOS
|
||||
const reactDevToolsPath = path.join(
|
||||
|
||||
@@ -4,15 +4,48 @@ Electron Forge is a tool for packaging and publishing Electron applications.
|
||||
It unifies Electron's build tooling ecosystem into
|
||||
a single extensible interface so that anyone can jump right into making Electron apps.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Alternative tooling</summary>
|
||||
|
||||
If you do not want to use Electron Forge for your project, there are other
|
||||
third-party tools you can use to distribute your app.
|
||||
|
||||
These tools are maintained by members of the Electron community,
|
||||
and do not come with official support from the Electron project.
|
||||
|
||||
**Electron Builder**
|
||||
|
||||
A "complete solution to package and build a ready-for-distribution Electron app"
|
||||
that focuses on an integrated experience. [`electron-builder`](https://github.com/electron-userland/electron-builder) adds a single dependency and manages all further requirements internally.
|
||||
|
||||
`electron-builder` replaces features and modules used by the Electron
|
||||
maintainers (such as the auto-updater) with custom ones.
|
||||
|
||||
**Hydraulic Conveyor**
|
||||
|
||||
A [desktop app deployment tool](https://hydraulic.dev) that supports
|
||||
cross-building/signing of all packages from any OS without the need for
|
||||
multi-platform CI, can do synchronous web-style updates on each start
|
||||
of the app, requires no code changes, can use plain HTTP servers for updates and
|
||||
which focuses on ease of use. Conveyor replaces the Electron auto-updaters
|
||||
with Sparkle on macOS, MSIX on Windows, and Linux package repositories.
|
||||
|
||||
Conveyor is a commercial tool that is free for open source projects. There's
|
||||
an example of [how to package GitHub Desktop](https://hydraulic.dev/blog/8-packaging-electron-apps.html)
|
||||
which can be used for learning.
|
||||
|
||||
</details>
|
||||
|
||||
## Getting started
|
||||
|
||||
The [Electron Forge docs][] contain detailed information on taking your application
|
||||
from source code to your end users' machines.
|
||||
This includes:
|
||||
|
||||
* Packaging your application [(package)][]
|
||||
* Generating executables and installers for each OS [(make)][], and,
|
||||
* Publishing these files to online platforms to download [(publish)][].
|
||||
- Packaging your application [(package)][]
|
||||
- Generating executables and installers for each OS [(make)][], and,
|
||||
- Publishing these files to online platforms to download [(publish)][].
|
||||
|
||||
For beginners, we recommend following through Electron's [tutorial][] to develop, build,
|
||||
package and publish your first Electron app. If you have already developed an app on your machine
|
||||
@@ -20,11 +53,11 @@ and want to start on packaging and distribution, start from [step 5][] of the tu
|
||||
|
||||
## Getting help
|
||||
|
||||
* If you need help with developing your app, our [community Discord server][discord] is a great place
|
||||
to get advice from other Electron app developers.
|
||||
* If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
|
||||
to see if any existing issues match your problem. If not, feel free to fill out our bug report
|
||||
template and submit a new issue.
|
||||
- If you need help with developing your app, our [community Discord server][discord] is a great place
|
||||
to get advice from other Electron app developers.
|
||||
- If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
|
||||
to see if any existing issues match your problem. If not, feel free to fill out our bug report
|
||||
template and submit a new issue.
|
||||
|
||||
[Electron Forge Docs]: https://www.electronforge.io/
|
||||
[step 5]: ./tutorial-5-packaging.md
|
||||
|
||||
@@ -52,7 +52,7 @@ In the main process, set an IPC listener on the `set-title` channel with the `ip
|
||||
|
||||
```javascript {6-10,22} title='main.js (Main Process)'
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
// ...
|
||||
|
||||
@@ -183,7 +183,7 @@ provided to the renderer process. Please refer to
|
||||
|
||||
```javascript {6-13,25} title='main.js (Main Process)'
|
||||
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
// ...
|
||||
|
||||
@@ -378,7 +378,7 @@ target renderer.
|
||||
|
||||
```javascript {11-26} title='main.js (Main Process)'
|
||||
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
|
||||
@@ -27,7 +27,7 @@ control our application lifecycle and create a native browser window.
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow, shell } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
```
|
||||
|
||||
Next, we will proceed to register our application to handle all "`electron-fiddle://`" protocols.
|
||||
|
||||
@@ -303,7 +303,7 @@ without having to step through the isolated world.
|
||||
|
||||
```js title='main.js (Main Process)'
|
||||
const { BrowserWindow, app, MessageChannelMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Create a BrowserWindow with contextIsolation enabled.
|
||||
|
||||
@@ -22,7 +22,7 @@ In `preload.js` use the [`contextBridge`][] to inject a method `window.electron.
|
||||
|
||||
```js
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
startDrag: (fileName) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ To enable this mode, GPU acceleration has to be disabled by calling the
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/offscreen-rendering'
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
|
||||
app.disableHardwareAcceleration()
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ equally fictitious `foo-parser` module. In traditional Node.js development,
|
||||
you might write code that eagerly loads dependencies:
|
||||
|
||||
```js title='parser.js' @ts-expect-error=[2]
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
const fooParser = require('foo-parser')
|
||||
|
||||
class Parser {
|
||||
@@ -198,7 +198,7 @@ do this work a little later, when `getParsedFiles()` is actually called?
|
||||
|
||||
```js title='parser.js' @ts-expect-error=[20]
|
||||
// "fs" is likely already being loaded, so the `require()` call is cheap
|
||||
const fs = require('fs')
|
||||
const fs = require('node:fs')
|
||||
|
||||
class Parser {
|
||||
async getFiles () {
|
||||
|
||||
@@ -292,7 +292,7 @@ to the `webPreferences.preload` option in your existing `BrowserWindow` construc
|
||||
```js
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
// include the Node.js 'path' module at the top of your file
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
// modify your existing createWindow() function
|
||||
const createWindow = () => {
|
||||
@@ -358,7 +358,7 @@ The full code is available below:
|
||||
|
||||
// Modules to control application life and create native browser window
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const createWindow = () => {
|
||||
// Create the browser window.
|
||||
|
||||
@@ -26,8 +26,8 @@ the application via JumpList or dock menu, respectively.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/recent-documents'
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const createWindow = () => {
|
||||
const win = new BrowserWindow({
|
||||
|
||||
@@ -29,7 +29,7 @@ To set the represented file of window, you can use the
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/represented-file'
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const os = require('os')
|
||||
const os = require('node:os')
|
||||
|
||||
const createWindow = () => {
|
||||
const win = new BrowserWindow({
|
||||
|
||||
@@ -83,7 +83,7 @@ To attach this script to your renderer process, pass its path to the
|
||||
|
||||
```js {2,8-10} title="main.js"
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const createWindow = () => {
|
||||
const win = new BrowserWindow({
|
||||
@@ -204,7 +204,7 @@ you send out the `invoke` call from the renderer.
|
||||
|
||||
```js {1,15} title="main.js"
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const createWindow = () => {
|
||||
const win = new BrowserWindow({
|
||||
|
||||
@@ -184,7 +184,7 @@ allowing events such as `mouseleave` to be emitted:
|
||||
|
||||
```javascript title='main.js'
|
||||
const { BrowserWindow, ipcMain } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const win = new BrowserWindow({
|
||||
webPreferences: {
|
||||
|
||||
@@ -126,7 +126,7 @@ following lines:
|
||||
|
||||
```javascript
|
||||
const { BrowserWindow, nativeImage } = require('electron')
|
||||
const path = require('path')
|
||||
const path = require('node:path')
|
||||
|
||||
const win = new BrowserWindow()
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ template("electron_extra_paks") {
|
||||
"$root_gen_dir/content/browser/tracing/tracing_resources.pak",
|
||||
"$root_gen_dir/content/browser/webrtc/resources/webrtc_internals_resources.pak",
|
||||
"$root_gen_dir/content/content_resources.pak",
|
||||
"$root_gen_dir/content/gpu_resources.pak",
|
||||
"$root_gen_dir/mojo/public/js/mojo_bindings_resources.pak",
|
||||
"$root_gen_dir/net/net_resources.pak",
|
||||
"$root_gen_dir/third_party/blink/public/resources/blink_resources.pak",
|
||||
@@ -74,6 +75,7 @@ template("electron_extra_paks") {
|
||||
"//chrome/common:resources",
|
||||
"//components/resources",
|
||||
"//content:content_resources",
|
||||
"//content/browser/resources/gpu:resources",
|
||||
"//content/browser/resources/media:resources",
|
||||
"//content/browser/tracing:resources",
|
||||
"//content/browser/webrtc/resources",
|
||||
|
||||
@@ -115,6 +115,7 @@ auto_filenames = {
|
||||
"docs/api/structures/protocol-response.md",
|
||||
"docs/api/structures/rectangle.md",
|
||||
"docs/api/structures/referrer.md",
|
||||
"docs/api/structures/render-process-gone-details.md",
|
||||
"docs/api/structures/resolved-endpoint.md",
|
||||
"docs/api/structures/resolved-host.md",
|
||||
"docs/api/structures/scrubber-item.md",
|
||||
|
||||
@@ -682,6 +682,8 @@ filenames = {
|
||||
"shell/browser/extensions/api/resources_private/resources_private_api.h",
|
||||
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.cc",
|
||||
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.h",
|
||||
"shell/browser/extensions/api/scripting/scripting_api.cc",
|
||||
"shell/browser/extensions/api/scripting/scripting_api.h",
|
||||
"shell/browser/extensions/api/streams_private/streams_private_api.cc",
|
||||
"shell/browser/extensions/api/streams_private/streams_private_api.h",
|
||||
"shell/browser/extensions/api/tabs/tabs_api.cc",
|
||||
|
||||
@@ -386,17 +386,6 @@ WebContents.prototype.print = function (options: ElectronInternal.WebContentsPri
|
||||
}
|
||||
};
|
||||
|
||||
WebContents.prototype.getPrinters = function () {
|
||||
// TODO(nornagon): this API has nothing to do with WebContents and should be
|
||||
// moved.
|
||||
if (printing.getPrinterList) {
|
||||
return printing.getPrinterList();
|
||||
} else {
|
||||
console.error('Error: Printing feature is disabled.');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
WebContents.prototype.getPrintersAsync = async function () {
|
||||
// TODO(nornagon): this API has nothing to do with WebContents and should be
|
||||
// moved.
|
||||
@@ -446,6 +435,7 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
};
|
||||
|
||||
let navigationStarted = false;
|
||||
let browserInitiatedInPageNavigation = false;
|
||||
const navigationListener = (event: Electron.Event, url: string, isSameDocument: boolean, isMainFrame: boolean) => {
|
||||
if (isMainFrame) {
|
||||
if (navigationStarted && !isSameDocument) {
|
||||
@@ -460,6 +450,7 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
// as the routing does not leave the document
|
||||
return rejectAndCleanup(-3, 'ERR_ABORTED', url);
|
||||
}
|
||||
browserInitiatedInPageNavigation = navigationStarted && isSameDocument;
|
||||
navigationStarted = true;
|
||||
}
|
||||
};
|
||||
@@ -474,17 +465,22 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||
// would be more appropriate.
|
||||
rejectAndCleanup(-2, 'ERR_FAILED', url);
|
||||
};
|
||||
const finishListenerWhenUserInitiatedNavigation = () => {
|
||||
if (!browserInitiatedInPageNavigation) {
|
||||
finishListener();
|
||||
}
|
||||
};
|
||||
const removeListeners = () => {
|
||||
this.removeListener('did-finish-load', finishListener);
|
||||
this.removeListener('did-fail-load', failListener);
|
||||
this.removeListener('did-navigate-in-page', finishListener);
|
||||
this.removeListener('did-navigate-in-page', finishListenerWhenUserInitiatedNavigation);
|
||||
this.removeListener('did-start-navigation', navigationListener);
|
||||
this.removeListener('did-stop-loading', stopLoadingListener);
|
||||
this.removeListener('destroyed', stopLoadingListener);
|
||||
};
|
||||
this.on('did-finish-load', finishListener);
|
||||
this.on('did-fail-load', failListener);
|
||||
this.on('did-navigate-in-page', finishListener);
|
||||
this.on('did-navigate-in-page', finishListenerWhenUserInitiatedNavigation);
|
||||
this.on('did-start-navigation', navigationListener);
|
||||
this.on('did-stop-loading', stopLoadingListener);
|
||||
this.on('destroyed', stopLoadingListener);
|
||||
|
||||
@@ -98,7 +98,6 @@ make_gtk_getlibgtk_public.patch
|
||||
build_disable_print_content_analysis.patch
|
||||
custom_protocols_plzserviceworker.patch
|
||||
feat_filter_out_non-shareable_windows_in_the_current_application_in.patch
|
||||
fix_allow_guest_webcontents_to_enter_fullscreen.patch
|
||||
disable_freezing_flags_after_init_in_node.patch
|
||||
short-circuit_permissions_checks_in_mediastreamdevicescontroller.patch
|
||||
chore_add_electron_deps_to_gitignores.patch
|
||||
@@ -129,3 +128,4 @@ fix_harden_blink_scriptstate_maybefrom.patch
|
||||
chore_add_buildflag_guard_around_new_include.patch
|
||||
fix_use_delegated_generic_capturer_when_available.patch
|
||||
build_remove_ent_content_analysis_assert.patch
|
||||
revert_remove_the_allowaggressivethrottlingwithwebsocket_feature.patch
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <sattard@salesforce.com>
|
||||
Date: Mon, 6 Jun 2022 14:25:15 -0700
|
||||
Subject: fix: allow guest webcontents to enter fullscreen
|
||||
|
||||
This can be upstreamed, a guest webcontents can't technically become the focused webContents. This DCHECK should allow all guest webContents to request fullscreen entrance.
|
||||
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 8d5a0e6aa6ea3a0965ed6abb76368d1f8e3ea91e..36cd52f18c83adc007d6f896a4136a93f0fa1c83 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -3715,7 +3715,7 @@ void WebContentsImpl::EnterFullscreenMode(
|
||||
OPTIONAL_TRACE_EVENT0("content", "WebContentsImpl::EnterFullscreenMode");
|
||||
DCHECK(CanEnterFullscreenMode(requesting_frame, options));
|
||||
DCHECK(requesting_frame->IsActive());
|
||||
- DCHECK(ContainsOrIsFocusedWebContents());
|
||||
+ DCHECK(ContainsOrIsFocusedWebContents() || IsGuest());
|
||||
|
||||
// When WebView is the `delegate_` we can end up with VisualProperties changes
|
||||
// synchronously. Notify the view ahead so it can handle the transition.
|
||||
@@ -45,10 +45,10 @@ index 1e9dcbb9f0a5e401a50d63be490755bc3eeaa1a3..1e2e0a321df6fe9dea4401ed327c9373
|
||||
// RenderFrameMetadataProvider::Observer implementation.
|
||||
void OnRenderFrameMetadataChangedBeforeActivation(
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 36cd52f18c83adc007d6f896a4136a93f0fa1c83..99f6e45fe48449c2dbe88514648a4d907999e282 100644
|
||||
index 61d985829d54e2f98351f39d1b58e5702f10fa79..6ee703fc50459901068c364a3a0ccd3a60ce28bb 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -8319,7 +8319,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame(
|
||||
@@ -8323,7 +8323,7 @@ void WebContentsImpl::OnFocusedElementChangedInFrame(
|
||||
"WebContentsImpl::OnFocusedElementChangedInFrame",
|
||||
"render_frame_host", frame);
|
||||
RenderWidgetHostViewBase* root_view =
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <samuel.r.attard@gmail.com>
|
||||
Date: Tue, 5 Sep 2023 13:22:31 -0700
|
||||
Subject: Revert "Remove the AllowAggressiveThrottlingWithWebSocket feature."
|
||||
|
||||
This reverts commit 615c1810a187840ffeb04096087efff86edb37de.
|
||||
|
||||
diff --git a/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc b/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
|
||||
index d566c1a209aa5b39692fe79dd8b5012a5e053574..90e97f739b3e7ede2fa4ceffb1be5130f651f60b 100644
|
||||
--- a/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
|
||||
+++ b/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
|
||||
@@ -95,6 +95,17 @@ enum WebSocketOpCode {
|
||||
kOpCodeBinary = 0x2,
|
||||
};
|
||||
|
||||
+// When enabled, a page can be aggressively throttled even if it uses a
|
||||
+// WebSocket. Aggressive throttling does not affect the execution of WebSocket
|
||||
+// event handlers, so there is little reason to disable it on pages using a
|
||||
+// WebSocket.
|
||||
+//
|
||||
+// TODO(crbug.com/1121725): Cleanup this feature in June 2021, when it becomes
|
||||
+// enabled by default on Stable.
|
||||
+BASE_FEATURE(kAllowAggressiveThrottlingWithWebSocket,
|
||||
+ "AllowAggressiveThrottlingWithWebSocket",
|
||||
+ base::FEATURE_ENABLED_BY_DEFAULT);
|
||||
+
|
||||
} // namespace
|
||||
|
||||
void WebSocketChannelImpl::MessageDataDeleter::operator()(char* p) const {
|
||||
@@ -285,7 +296,10 @@ bool WebSocketChannelImpl::Connect(const KURL& url, const String& protocol) {
|
||||
// even if the `WebSocketChannel` is closed.
|
||||
feature_handle_for_scheduler_ = scheduler->RegisterFeature(
|
||||
SchedulingPolicy::Feature::kWebSocket,
|
||||
- SchedulingPolicy{SchedulingPolicy::DisableBackForwardCache()});
|
||||
+ base::FeatureList::IsEnabled(kAllowAggressiveThrottlingWithWebSocket)
|
||||
+ ? SchedulingPolicy{SchedulingPolicy::DisableBackForwardCache()}
|
||||
+ : SchedulingPolicy{SchedulingPolicy::DisableAggressiveThrottling(),
|
||||
+ SchedulingPolicy::DisableBackForwardCache()});
|
||||
scheduler->RegisterStickyFeature(
|
||||
SchedulingPolicy::Feature::kWebSocketSticky,
|
||||
SchedulingPolicy{SchedulingPolicy::DisableBackForwardCache()});
|
||||
@@ -8,7 +8,8 @@ the parent frame should also enter fullscreen mode too. Chromium handles
|
||||
this for iframes, but not for webviews as they are essentially main
|
||||
frames instead of child frames.
|
||||
|
||||
This patch makes webviews propagate the fullscreen state to embedder.
|
||||
This patch makes webviews propagate the fullscreen state to embedder.It also handles a
|
||||
DCHECK preventing guest webcontents from becoming the focused webContents.
|
||||
|
||||
Note that we also need to manually update embedder's
|
||||
`api::WebContents::IsFullscreenForTabOrPending` value.
|
||||
@@ -35,3 +36,67 @@ index 01eb116b51037bff5da7a87d119b78387f5e72c6..7ab5609215ce352b2595b4953e1b9ca0
|
||||
// Focus the window if another frame may have delegated the capability.
|
||||
if (had_fullscreen_token && !GetView()->HasFocus())
|
||||
GetView()->Focus();
|
||||
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
|
||||
index 8d5a0e6aa6ea3a0965ed6abb76368d1f8e3ea91e..61d985829d54e2f98351f39d1b58e5702f10fa79 100644
|
||||
--- a/content/browser/web_contents/web_contents_impl.cc
|
||||
+++ b/content/browser/web_contents/web_contents_impl.cc
|
||||
@@ -3569,21 +3569,25 @@ KeyboardEventProcessingResult WebContentsImpl::PreHandleKeyboardEvent(
|
||||
const NativeWebKeyboardEvent& event) {
|
||||
OPTIONAL_TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("content.verbose"),
|
||||
"WebContentsImpl::PreHandleKeyboardEvent");
|
||||
- auto* outermost_contents = GetOutermostWebContents();
|
||||
- // TODO(wjmaclean): Generalize this to forward all key events to the outermost
|
||||
- // delegate's handler.
|
||||
- if (outermost_contents != this && IsFullscreen() &&
|
||||
- event.windows_key_code == ui::VKEY_ESCAPE) {
|
||||
- // When an inner WebContents has focus and is fullscreen, redirect <esc>
|
||||
- // key events to the outermost WebContents so it can be handled by that
|
||||
- // WebContents' delegate.
|
||||
- if (outermost_contents->PreHandleKeyboardEvent(event) ==
|
||||
- KeyboardEventProcessingResult::HANDLED) {
|
||||
+
|
||||
+ auto handled = delegate_ ? delegate_->PreHandleKeyboardEvent(this, event)
|
||||
+ : KeyboardEventProcessingResult::NOT_HANDLED;
|
||||
+
|
||||
+ if (IsFullscreen() && event.windows_key_code == ui::VKEY_ESCAPE) {
|
||||
+ if (handled == KeyboardEventProcessingResult::HANDLED)
|
||||
return KeyboardEventProcessingResult::HANDLED;
|
||||
+
|
||||
+ // When an inner WebContents has focus and is fullscreen, traverse through
|
||||
+ // containing webcontents to any that may handle the escape key.
|
||||
+ while (auto* outer_web_contents = GetOuterWebContents()) {
|
||||
+ auto result = outer_web_contents->PreHandleKeyboardEvent(event);
|
||||
+ if (result == KeyboardEventProcessingResult::HANDLED) {
|
||||
+ return KeyboardEventProcessingResult::HANDLED;
|
||||
+ }
|
||||
}
|
||||
}
|
||||
- return delegate_ ? delegate_->PreHandleKeyboardEvent(this, event)
|
||||
- : KeyboardEventProcessingResult::NOT_HANDLED;
|
||||
+
|
||||
+ return handled;
|
||||
}
|
||||
|
||||
bool WebContentsImpl::HandleMouseEvent(const blink::WebMouseEvent& event) {
|
||||
@@ -3715,7 +3719,7 @@ void WebContentsImpl::EnterFullscreenMode(
|
||||
OPTIONAL_TRACE_EVENT0("content", "WebContentsImpl::EnterFullscreenMode");
|
||||
DCHECK(CanEnterFullscreenMode(requesting_frame, options));
|
||||
DCHECK(requesting_frame->IsActive());
|
||||
- DCHECK(ContainsOrIsFocusedWebContents());
|
||||
+ DCHECK(ContainsOrIsFocusedWebContents() || IsGuest());
|
||||
|
||||
// When WebView is the `delegate_` we can end up with VisualProperties changes
|
||||
// synchronously. Notify the view ahead so it can handle the transition.
|
||||
diff --git a/third_party/blink/renderer/core/fullscreen/fullscreen.cc b/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
index f58ade7ed652ea0d97c123dc78715c049cb5e500..6619207f2f8731a7d8e88d6c1613e77ca4abc558 100644
|
||||
--- a/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
+++ b/third_party/blink/renderer/core/fullscreen/fullscreen.cc
|
||||
@@ -99,7 +99,7 @@ void FullscreenElementChanged(Document& document,
|
||||
// is the iframe element for the out-of-process frame that contains the
|
||||
// fullscreen element. Hence, it must match :-webkit-full-screen-ancestor.
|
||||
if (new_request_type & FullscreenRequestType::kForCrossProcessDescendant) {
|
||||
- DCHECK(IsA<HTMLIFrameElement>(new_element));
|
||||
+ // DCHECK(IsA<HTMLIFrameElement>(new_element));
|
||||
new_element->SetContainsFullScreenElement(true);
|
||||
}
|
||||
new_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(
|
||||
|
||||
@@ -41,3 +41,4 @@ fix_ftbfs_werror_wextra-semi.patch
|
||||
fix_isurl_implementation.patch
|
||||
ci_ensure_node_tests_set_electron_run_as_node.patch
|
||||
chore_update_fixtures_errors_force_colors_snapshot.patch
|
||||
fix_assert_module_in_the_renderer_process.patch
|
||||
|
||||
75
patches/node/fix_assert_module_in_the_renderer_process.patch
Normal file
75
patches/node/fix_assert_module_in_the_renderer_process.patch
Normal file
@@ -0,0 +1,75 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Shelley Vohr <shelley.vohr@gmail.com>
|
||||
Date: Wed, 16 Aug 2023 19:15:29 +0200
|
||||
Subject: fix: assert module in the renderer process
|
||||
|
||||
When creating a Node.js Environment, embedders have the option to disable Node.js'
|
||||
default overriding of Error.prepareStackTrace. However, the assert module depends on
|
||||
a WeakMap that is populated with the error stacktraces in the overridden function.
|
||||
|
||||
This adds handling to fall back to the default implementation if Error.prepareStackTrace
|
||||
if the override has been disabled.
|
||||
|
||||
This will be upstreamed.
|
||||
|
||||
diff --git a/lib/assert.js b/lib/assert.js
|
||||
index 04c2dd3bfcfdfbb4b8079c306e1d80aa48027787..34658819d09cc20f372798caec79e19c4a36565d 100644
|
||||
--- a/lib/assert.js
|
||||
+++ b/lib/assert.js
|
||||
@@ -66,6 +66,7 @@ const { inspect } = require('internal/util/inspect');
|
||||
const { isPromise, isRegExp } = require('internal/util/types');
|
||||
const { EOL } = require('internal/constants');
|
||||
const { BuiltinModule } = require('internal/bootstrap/loaders');
|
||||
+const { getEmbedderOptions } = require('internal/options');
|
||||
const { isError } = require('internal/util');
|
||||
|
||||
const errorCache = new SafeMap();
|
||||
@@ -293,8 +294,16 @@ function getErrMessage(message, fn) {
|
||||
ErrorCaptureStackTrace(err, fn);
|
||||
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
|
||||
|
||||
- overrideStackTrace.set(err, (_, stack) => stack);
|
||||
- const call = err.stack[0];
|
||||
+ let call;
|
||||
+ if (getEmbedderOptions().hasPrepareStackTraceCallback) {
|
||||
+ overrideStackTrace.set(err, (_, stack) => stack);
|
||||
+ call = err.stack[0];
|
||||
+ } else {
|
||||
+ const tmpPrepare = Error.prepareStackTrace;
|
||||
+ Error.prepareStackTrace = (_, stack) => stack;
|
||||
+ call = err.stack[0];
|
||||
+ Error.prepareStackTrace = tmpPrepare;
|
||||
+ }
|
||||
|
||||
const filename = call.getFileName();
|
||||
const line = call.getLineNumber() - 1;
|
||||
diff --git a/src/api/environment.cc b/src/api/environment.cc
|
||||
index d5a03d5e10faaa204b3f9f290fed79be824c78b1..c4caef25af670658965fc740ce03c2d2c4ed3e66 100644
|
||||
--- a/src/api/environment.cc
|
||||
+++ b/src/api/environment.cc
|
||||
@@ -263,6 +263,9 @@ void SetIsolateErrorHandlers(v8::Isolate* isolate, const IsolateSettings& s) {
|
||||
auto* prepare_stack_trace_cb = s.prepare_stack_trace_callback ?
|
||||
s.prepare_stack_trace_callback : PrepareStackTraceCallback;
|
||||
isolate->SetPrepareStackTraceCallback(prepare_stack_trace_cb);
|
||||
+ } else {
|
||||
+ auto env = Environment::GetCurrent(isolate);
|
||||
+ env->set_prepare_stack_trace_callback(Local<Function>());
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/src/node_options.cc b/src/node_options.cc
|
||||
index 499d048fcb82e50a302d74e2c7f87f3696103a40..7ad8d80faee840e4dd224d946871b2ff08b0c23c 100644
|
||||
--- a/src/node_options.cc
|
||||
+++ b/src/node_options.cc
|
||||
@@ -1205,6 +1205,11 @@ void GetEmbedderOptions(const FunctionCallbackInfo<Value>& args) {
|
||||
Local<Context> context = env->context();
|
||||
Local<Object> ret = Object::New(isolate);
|
||||
|
||||
+ if (ret->Set(context,
|
||||
+ FIXED_ONE_BYTE_STRING(env->isolate(), "hasPrepareStackTraceCallback"),
|
||||
+ Boolean::New(isolate, !env->prepare_stack_trace_callback().IsEmpty()))
|
||||
+ .IsNothing()) return;
|
||||
+
|
||||
if (ret->Set(context,
|
||||
FIXED_ONE_BYTE_STRING(env->isolate(), "shouldNotRegisterESMLoader"),
|
||||
Boolean::New(isolate, env->should_not_register_esm_loader()))
|
||||
@@ -1,6 +1,6 @@
|
||||
param([string]$gomaDir=$PWD)
|
||||
$cmdPath = Join-Path -Path $gomaDir -ChildPath "goma_ctl.py"
|
||||
Start-Process -FilePath cmd -ArgumentList "/C", "python", "$cmdPath", "ensure_start"
|
||||
Start-Process -FilePath cmd -ArgumentList "/C", "python3", "$cmdPath", "ensure_start"
|
||||
$timedOut = $false; $waitTime = 0; $waitIncrement = 5; $timeout=120;
|
||||
Do { sleep $waitIncrement; $timedOut = (($waitTime+=$waitIncrement) -gt $timeout); iex "$gomaDir\gomacc.exe port 2" > $null; } Until(($LASTEXITCODE -eq 0) -or $timedOut)
|
||||
if ($timedOut) {
|
||||
|
||||
@@ -54,13 +54,14 @@ namespace {
|
||||
// See https://nodejs.org/api/cli.html#cli_options
|
||||
void ExitIfContainsDisallowedFlags(const std::vector<std::string>& argv) {
|
||||
// Options that are unilaterally disallowed.
|
||||
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
static constexpr auto disallowed =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
|
||||
for (const auto& arg : argv) {
|
||||
const auto key = base::StringPiece(arg).substr(0, arg.find('='));
|
||||
|
||||
@@ -768,10 +768,20 @@ void BaseWindow::AddBrowserView(gin::Handle<BrowserView> browser_view) {
|
||||
browser_view->SetOwnerWindow(nullptr);
|
||||
}
|
||||
|
||||
// If the user set the BrowserView's bounds before adding it to the window,
|
||||
// we need to get those initial bounds *before* adding it to the window
|
||||
// so bounds isn't returned relative despite not being correctly positioned
|
||||
// relative to the window.
|
||||
auto bounds = browser_view->GetBounds();
|
||||
|
||||
window_->AddBrowserView(browser_view->view());
|
||||
window_->AddDraggableRegionProvider(browser_view.get());
|
||||
browser_view->SetOwnerWindow(this);
|
||||
browser_views_.emplace_back().Reset(isolate(), browser_view.ToV8());
|
||||
|
||||
// Recalibrate bounds relative to the containing window.
|
||||
if (!bounds.IsEmpty())
|
||||
browser_view->SetBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ class BrowserView : public gin::Wrappable<BrowserView>,
|
||||
BrowserView(const BrowserView&) = delete;
|
||||
BrowserView& operator=(const BrowserView&) = delete;
|
||||
|
||||
gfx::Rect GetBounds();
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
|
||||
protected:
|
||||
BrowserView(gin::Arguments* args, const gin_helper::Dictionary& options);
|
||||
~BrowserView() override;
|
||||
@@ -78,8 +81,6 @@ class BrowserView : public gin::Wrappable<BrowserView>,
|
||||
|
||||
private:
|
||||
void SetAutoResize(AutoResizeFlags flags);
|
||||
void SetBounds(const gfx::Rect& bounds);
|
||||
gfx::Rect GetBounds();
|
||||
void SetBackgroundColor(const std::string& color_name);
|
||||
v8::Local<v8::Value> GetWebContents(v8::Isolate*);
|
||||
|
||||
|
||||
@@ -41,25 +41,6 @@ struct Converter<printing::PrinterBasicInfo> {
|
||||
namespace electron::api {
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
printing::PrinterList GetPrinterList(v8::Isolate* isolate) {
|
||||
EmitWarning(node::Environment::GetCurrent(isolate),
|
||||
"Deprecation Warning: getPrinters() is deprecated. "
|
||||
"Use the asynchronous and non-blocking version, "
|
||||
"getPrintersAsync(), instead.",
|
||||
"electron");
|
||||
printing::PrinterList printers;
|
||||
auto print_backend = printing::PrintBackend::CreateInstance(
|
||||
g_browser_process->GetApplicationLocale());
|
||||
{
|
||||
ScopedAllowBlockingForElectron allow_blocking;
|
||||
printing::mojom::ResultCode code =
|
||||
print_backend->EnumeratePrinters(printers);
|
||||
if (code != printing::mojom::ResultCode::kSuccess)
|
||||
LOG(INFO) << "Failed to enumerate printers";
|
||||
}
|
||||
return printers;
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> GetPrinterListAsync(v8::Isolate* isolate) {
|
||||
gin_helper::Promise<printing::PrinterList> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
@@ -92,7 +73,6 @@ v8::Local<v8::Promise> GetPrinterListAsync(v8::Isolate* isolate) {
|
||||
namespace {
|
||||
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
using electron::api::GetPrinterList;
|
||||
using electron::api::GetPrinterListAsync;
|
||||
#endif
|
||||
|
||||
@@ -103,7 +83,6 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
gin_helper::Dictionary dict(isolate, exports);
|
||||
#if BUILDFLAG(ENABLE_PRINTING)
|
||||
dict.SetMethod("getPrinterList", base::BindRepeating(&GetPrinterList));
|
||||
dict.SetMethod("getPrinterListAsync",
|
||||
base::BindRepeating(&GetPrinterListAsync));
|
||||
#endif
|
||||
|
||||
@@ -2444,12 +2444,25 @@ void WebContents::ReloadIgnoringCache() {
|
||||
/* check_for_repost */ true);
|
||||
}
|
||||
|
||||
void WebContents::DownloadURL(const GURL& url) {
|
||||
auto* browser_context = web_contents()->GetBrowserContext();
|
||||
auto* download_manager = browser_context->GetDownloadManager();
|
||||
void WebContents::DownloadURL(const GURL& url, gin::Arguments* args) {
|
||||
std::map<std::string, std::string> headers;
|
||||
gin_helper::Dictionary options;
|
||||
if (args->GetNext(&options)) {
|
||||
if (options.Has("headers") && !options.Get("headers", &headers)) {
|
||||
args->ThrowTypeError("Invalid value for headers - must be an object");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<download::DownloadUrlParameters> download_params(
|
||||
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
|
||||
web_contents(), url, MISSING_TRAFFIC_ANNOTATION));
|
||||
for (const auto& [name, value] : headers) {
|
||||
download_params->add_request_header(name, value);
|
||||
}
|
||||
|
||||
auto* download_manager =
|
||||
web_contents()->GetBrowserContext()->GetDownloadManager();
|
||||
download_manager->DownloadUrl(std::move(download_params));
|
||||
}
|
||||
|
||||
@@ -2640,14 +2653,6 @@ void WebContents::OpenDevTools(gin::Arguments* args) {
|
||||
state = "detach";
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
auto* win = static_cast<NativeWindowViews*>(owner_window());
|
||||
// Force a detached state when WCO is enabled to match Chrome
|
||||
// behavior and prevent occlusion of DevTools.
|
||||
if (win && win->IsWindowControlsOverlayEnabled())
|
||||
state = "detach";
|
||||
#endif
|
||||
|
||||
bool activate = true;
|
||||
std::string title;
|
||||
if (args && args->Length() == 1) {
|
||||
|
||||
@@ -169,7 +169,7 @@ class WebContents : public ExclusiveAccessContext,
|
||||
void LoadURL(const GURL& url, const gin_helper::Dictionary& options);
|
||||
void Reload();
|
||||
void ReloadIgnoringCache();
|
||||
void DownloadURL(const GURL& url);
|
||||
void DownloadURL(const GURL& url, gin::Arguments* args);
|
||||
GURL GetURL() const;
|
||||
std::u16string GetTitle() const;
|
||||
bool IsLoading() const;
|
||||
|
||||
@@ -44,13 +44,6 @@ namespace electron {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsAppRTL() {
|
||||
const std::string& locale = g_browser_process->GetApplicationLocale();
|
||||
base::i18n::TextDirection text_direction =
|
||||
base::i18n::GetTextDirectionForLocaleInStartUp(locale.c_str());
|
||||
return text_direction == base::i18n::RIGHT_TO_LEFT;
|
||||
}
|
||||
|
||||
NSString* GetAppPathForProtocol(const GURL& url) {
|
||||
NSURL* ns_url = [NSURL
|
||||
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
|
||||
|
||||
@@ -580,7 +580,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
|
||||
blink::MediaStreamDevice video_device(request.video_type, id, name);
|
||||
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
|
||||
nullptr, url::Origin::Create(request.security_origin),
|
||||
content::DesktopMediaID::Parse(request.requested_video_device_id));
|
||||
content::DesktopMediaID::Parse(video_device.id));
|
||||
devices.video_device = video_device;
|
||||
} else if (result_dict.Get("video", &rfh)) {
|
||||
auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
|
||||
@@ -592,7 +592,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
|
||||
base::UTF16ToUTF8(web_contents->GetTitle()));
|
||||
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
|
||||
web_contents, url::Origin::Create(request.security_origin),
|
||||
content::DesktopMediaID::Parse(request.requested_video_device_id));
|
||||
content::DesktopMediaID::Parse(video_device.id));
|
||||
devices.video_device = video_device;
|
||||
} else {
|
||||
gin_helper::ErrorThrower(args->isolate())
|
||||
|
||||
@@ -204,11 +204,11 @@ ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr;
|
||||
|
||||
ElectronBrowserMainParts::ElectronBrowserMainParts()
|
||||
: fake_browser_process_(std::make_unique<BrowserProcessImpl>()),
|
||||
browser_(std::make_unique<Browser>()),
|
||||
node_bindings_(
|
||||
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)),
|
||||
electron_bindings_(
|
||||
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())) {
|
||||
node_bindings_{
|
||||
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)},
|
||||
electron_bindings_{
|
||||
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())},
|
||||
browser_{std::make_unique<Browser>()} {
|
||||
DCHECK(!self_) << "Cannot have two ElectronBrowserMainParts";
|
||||
self_ = this;
|
||||
}
|
||||
@@ -266,26 +266,25 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
|
||||
|
||||
node_bindings_->Initialize(js_env_->isolate()->GetCurrentContext());
|
||||
// Create the global environment.
|
||||
node::Environment* env = node_bindings_->CreateEnvironment(
|
||||
node_env_ = node_bindings_->CreateEnvironment(
|
||||
js_env_->isolate()->GetCurrentContext(), js_env_->platform());
|
||||
node_env_ = std::make_unique<NodeEnvironment>(env);
|
||||
|
||||
env->set_trace_sync_io(env->options()->trace_sync_io);
|
||||
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
|
||||
|
||||
// We do not want to crash the main process on unhandled rejections.
|
||||
env->options()->unhandled_rejections = "warn-with-error-code";
|
||||
node_env_->options()->unhandled_rejections = "warn-with-error-code";
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
|
||||
electron_bindings_->BindTo(js_env_->isolate(), node_env_->process_object());
|
||||
|
||||
// Create explicit microtasks runner.
|
||||
js_env_->CreateMicrotasksRunner();
|
||||
|
||||
// Wrap the uv loop with global env.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(node_env_.get());
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(node_env_.get());
|
||||
|
||||
// We already initialized the feature list in PreEarlyInitialization(), but
|
||||
// the user JS script would not have had a chance to alter the command-line
|
||||
@@ -627,9 +626,9 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
|
||||
|
||||
// Destroy node platform after all destructors_ are executed, as they may
|
||||
// invoke Node/V8 APIs inside them.
|
||||
node_env_->env()->set_trace_sync_io(false);
|
||||
node_env_->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node_env_.reset();
|
||||
|
||||
auto default_context_key = ElectronBrowserContext::PartitionKey("", false);
|
||||
|
||||
@@ -37,6 +37,10 @@ class Screen;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace node {
|
||||
class Environment;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
class LinuxUiGetter;
|
||||
}
|
||||
@@ -47,7 +51,6 @@ class Browser;
|
||||
class ElectronBindings;
|
||||
class JavascriptEnvironment;
|
||||
class NodeBindings;
|
||||
class NodeEnvironment;
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
class ElectronExtensionsClient;
|
||||
@@ -157,11 +160,20 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
// Before then, we just exit() without any intermediate steps.
|
||||
absl::optional<int> exit_code_;
|
||||
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
std::unique_ptr<Browser> browser_;
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
|
||||
// depends-on: node_bindings_
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::unique_ptr<NodeEnvironment> node_env_;
|
||||
|
||||
// depends-on: node_bindings_
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
|
||||
// depends-on: js_env_'s isolate
|
||||
std::shared_ptr<node::Environment> node_env_;
|
||||
|
||||
// depends-on: js_env_'s isolate
|
||||
std::unique_ptr<Browser> browser_;
|
||||
|
||||
std::unique_ptr<IconManager> icon_manager_;
|
||||
std::unique_ptr<base::FieldTrialList> field_trial_list_;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ function_registration("api_registration") {
|
||||
sources = [
|
||||
"//electron/shell/common/extensions/api/extension.json",
|
||||
"//electron/shell/common/extensions/api/resources_private.idl",
|
||||
"//electron/shell/common/extensions/api/scripting.idl",
|
||||
"//electron/shell/common/extensions/api/tabs.json",
|
||||
]
|
||||
|
||||
|
||||
1390
shell/browser/extensions/api/scripting/scripting_api.cc
Normal file
1390
shell/browser/extensions/api/scripting/scripting_api.cc
Normal file
File diff suppressed because it is too large
Load Diff
207
shell/browser/extensions/api/scripting/scripting_api.h
Normal file
207
shell/browser/extensions/api/scripting/scripting_api.h
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright 2023 Microsoft, GmbH
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_
|
||||
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "chrome/common/extensions/api/scripting.h"
|
||||
#include "extensions/browser/extension_function.h"
|
||||
#include "extensions/browser/script_executor.h"
|
||||
#include "extensions/common/mojom/code_injection.mojom.h"
|
||||
#include "extensions/common/user_script.h"
|
||||
#include "third_party/abseil-cpp/absl/types/optional.h"
|
||||
|
||||
namespace extensions {
|
||||
|
||||
// A simple helper struct to represent a read file (either CSS or JS) to be
|
||||
// injected.
|
||||
struct InjectedFileSource {
|
||||
InjectedFileSource(std::string file_name, std::unique_ptr<std::string> data);
|
||||
InjectedFileSource(InjectedFileSource&&);
|
||||
~InjectedFileSource();
|
||||
|
||||
std::string file_name;
|
||||
std::unique_ptr<std::string> data;
|
||||
};
|
||||
|
||||
class ScriptingExecuteScriptFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.executeScript", SCRIPTING_EXECUTESCRIPT)
|
||||
|
||||
ScriptingExecuteScriptFunction();
|
||||
ScriptingExecuteScriptFunction(const ScriptingExecuteScriptFunction&) =
|
||||
delete;
|
||||
ScriptingExecuteScriptFunction& operator=(
|
||||
const ScriptingExecuteScriptFunction&) = delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingExecuteScriptFunction() override;
|
||||
|
||||
// Called when the resource files to be injected has been loaded.
|
||||
void DidLoadResources(std::vector<InjectedFileSource> file_sources,
|
||||
absl::optional<std::string> load_error);
|
||||
|
||||
// Triggers the execution of `sources` in the appropriate context.
|
||||
// Returns true on success; on failure, populates `error`.
|
||||
bool Execute(std::vector<mojom::JSSourcePtr> sources, std::string* error);
|
||||
|
||||
// Invoked when script execution is complete.
|
||||
void OnScriptExecuted(std::vector<ScriptExecutor::FrameResult> frame_results);
|
||||
|
||||
api::scripting::ScriptInjection injection_;
|
||||
};
|
||||
|
||||
class ScriptingInsertCSSFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.insertCSS", SCRIPTING_INSERTCSS)
|
||||
|
||||
ScriptingInsertCSSFunction();
|
||||
ScriptingInsertCSSFunction(const ScriptingInsertCSSFunction&) = delete;
|
||||
ScriptingInsertCSSFunction& operator=(const ScriptingInsertCSSFunction&) =
|
||||
delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingInsertCSSFunction() override;
|
||||
|
||||
// Called when the resource files to be injected has been loaded.
|
||||
void DidLoadResources(std::vector<InjectedFileSource> file_sources,
|
||||
absl::optional<std::string> load_error);
|
||||
|
||||
// Triggers the execution of `sources` in the appropriate context.
|
||||
// Returns true on success; on failure, populates `error`.
|
||||
bool Execute(std::vector<mojom::CSSSourcePtr> sources, std::string* error);
|
||||
|
||||
// Called when the CSS insertion is complete.
|
||||
void OnCSSInserted(std::vector<ScriptExecutor::FrameResult> results);
|
||||
|
||||
api::scripting::CSSInjection injection_;
|
||||
};
|
||||
|
||||
class ScriptingRemoveCSSFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.removeCSS", SCRIPTING_REMOVECSS)
|
||||
|
||||
ScriptingRemoveCSSFunction();
|
||||
ScriptingRemoveCSSFunction(const ScriptingRemoveCSSFunction&) = delete;
|
||||
ScriptingRemoveCSSFunction& operator=(const ScriptingRemoveCSSFunction&) =
|
||||
delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingRemoveCSSFunction() override;
|
||||
|
||||
// Called when the CSS removal is complete.
|
||||
void OnCSSRemoved(std::vector<ScriptExecutor::FrameResult> results);
|
||||
};
|
||||
|
||||
using ValidateContentScriptsResult =
|
||||
std::pair<std::unique_ptr<UserScriptList>, absl::optional<std::string>>;
|
||||
|
||||
class ScriptingRegisterContentScriptsFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.registerContentScripts",
|
||||
SCRIPTING_REGISTERCONTENTSCRIPTS)
|
||||
|
||||
ScriptingRegisterContentScriptsFunction();
|
||||
ScriptingRegisterContentScriptsFunction(
|
||||
const ScriptingRegisterContentScriptsFunction&) = delete;
|
||||
ScriptingRegisterContentScriptsFunction& operator=(
|
||||
const ScriptingRegisterContentScriptsFunction&) = delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingRegisterContentScriptsFunction() override;
|
||||
|
||||
// Called when script files have been checked.
|
||||
void OnContentScriptFilesValidated(
|
||||
std::set<std::string> persistent_script_ids,
|
||||
ValidateContentScriptsResult result);
|
||||
|
||||
// Called when content scripts have been registered.
|
||||
void OnContentScriptsRegistered(const absl::optional<std::string>& error);
|
||||
};
|
||||
|
||||
class ScriptingGetRegisteredContentScriptsFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.getRegisteredContentScripts",
|
||||
SCRIPTING_GETREGISTEREDCONTENTSCRIPTS)
|
||||
|
||||
ScriptingGetRegisteredContentScriptsFunction();
|
||||
ScriptingGetRegisteredContentScriptsFunction(
|
||||
const ScriptingGetRegisteredContentScriptsFunction&) = delete;
|
||||
ScriptingGetRegisteredContentScriptsFunction& operator=(
|
||||
const ScriptingGetRegisteredContentScriptsFunction&) = delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingGetRegisteredContentScriptsFunction() override;
|
||||
};
|
||||
|
||||
class ScriptingUnregisterContentScriptsFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.unregisterContentScripts",
|
||||
SCRIPTING_UNREGISTERCONTENTSCRIPTS)
|
||||
|
||||
ScriptingUnregisterContentScriptsFunction();
|
||||
ScriptingUnregisterContentScriptsFunction(
|
||||
const ScriptingUnregisterContentScriptsFunction&) = delete;
|
||||
ScriptingUnregisterContentScriptsFunction& operator=(
|
||||
const ScriptingUnregisterContentScriptsFunction&) = delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingUnregisterContentScriptsFunction() override;
|
||||
|
||||
// Called when content scripts have been unregistered.
|
||||
void OnContentScriptsUnregistered(const absl::optional<std::string>& error);
|
||||
};
|
||||
|
||||
class ScriptingUpdateContentScriptsFunction : public ExtensionFunction {
|
||||
public:
|
||||
DECLARE_EXTENSION_FUNCTION("scripting.updateContentScripts",
|
||||
SCRIPTING_UPDATECONTENTSCRIPTS)
|
||||
|
||||
ScriptingUpdateContentScriptsFunction();
|
||||
ScriptingUpdateContentScriptsFunction(
|
||||
const ScriptingUpdateContentScriptsFunction&) = delete;
|
||||
ScriptingUpdateContentScriptsFunction& operator=(
|
||||
const ScriptingUpdateContentScriptsFunction&) = delete;
|
||||
|
||||
// ExtensionFunction:
|
||||
ResponseAction Run() override;
|
||||
|
||||
private:
|
||||
~ScriptingUpdateContentScriptsFunction() override;
|
||||
|
||||
// Called when script files have been checked.
|
||||
void OnContentScriptFilesValidated(
|
||||
std::set<std::string> persistent_script_ids,
|
||||
ValidateContentScriptsResult result);
|
||||
|
||||
// Called when content scripts have been updated.
|
||||
void OnContentScriptsUpdated(const absl::optional<std::string>& error);
|
||||
};
|
||||
|
||||
} // namespace extensions
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_
|
||||
@@ -301,6 +301,7 @@ ExtensionFunction::ResponseAction TabsQueryFunction::Run() {
|
||||
|
||||
tabs::Tab tab;
|
||||
tab.id = contents->ID();
|
||||
tab.title = base::UTF16ToUTF8(wc->GetTitle());
|
||||
tab.url = wc->GetLastCommittedURL().spec();
|
||||
tab.active = contents->IsFocused();
|
||||
tab.audible = contents->IsCurrentlyAudible();
|
||||
@@ -322,12 +323,18 @@ ExtensionFunction::ResponseAction TabsGetFunction::Run() {
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
tabs::Tab tab;
|
||||
|
||||
tab.id = tab_id;
|
||||
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
|
||||
// that have the "tabs" (or "activeTab") permission. We should do the same
|
||||
// permission check here.
|
||||
tab.url = contents->web_contents()->GetLastCommittedURL().spec();
|
||||
|
||||
// "title" and "url" properties are considered privileged data and can
|
||||
// only be checked if the extension has the "tabs" permission or it has
|
||||
// access to the WebContents's origin.
|
||||
auto* wc = contents->web_contents();
|
||||
if (extension()->permissions_data()->HasAPIPermissionForTab(
|
||||
contents->ID(), mojom::APIPermissionID::kTab) ||
|
||||
extension()->permissions_data()->HasHostPermission(wc->GetURL())) {
|
||||
tab.url = wc->GetLastCommittedURL().spec();
|
||||
tab.title = base::UTF16ToUTF8(wc->GetTitle());
|
||||
}
|
||||
|
||||
tab.active = contents->IsFocused();
|
||||
|
||||
@@ -609,10 +616,16 @@ ExtensionFunction::ResponseValue TabsUpdateFunction::GetResult() {
|
||||
auto* api_web_contents = electron::api::WebContents::From(web_contents_);
|
||||
tab.id = (api_web_contents ? api_web_contents->ID() : -1);
|
||||
|
||||
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
|
||||
// that have the "tabs" (or "activeTab") permission. We should do the same
|
||||
// permission check here.
|
||||
tab.url = web_contents_->GetLastCommittedURL().spec();
|
||||
// "title" and "url" properties are considered privileged data and can
|
||||
// only be checked if the extension has the "tabs" permission or it has
|
||||
// access to the WebContents's origin.
|
||||
if (extension()->permissions_data()->HasAPIPermissionForTab(
|
||||
api_web_contents->ID(), mojom::APIPermissionID::kTab) ||
|
||||
extension()->permissions_data()->HasHostPermission(
|
||||
web_contents_->GetURL())) {
|
||||
tab.url = web_contents_->GetLastCommittedURL().spec();
|
||||
tab.title = base::UTF16ToUTF8(web_contents_->GetTitle());
|
||||
}
|
||||
|
||||
if (api_web_contents)
|
||||
tab.active = api_web_contents->IsFocused();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "extensions/browser/api/i18n/i18n_api.h"
|
||||
#include "extensions/browser/extension_function_registry.h"
|
||||
#include "shell/browser/extensions/api/generated_api_registration.h"
|
||||
#include "shell/browser/extensions/api/scripting/scripting_api.h"
|
||||
#include "shell/browser/extensions/api/tabs/tabs_api.h"
|
||||
|
||||
namespace extensions {
|
||||
|
||||
@@ -104,9 +104,10 @@ gin::IsolateHolder CreateIsolateHolder(v8::Isolate* isolate) {
|
||||
|
||||
JavascriptEnvironment::JavascriptEnvironment(uv_loop_t* event_loop,
|
||||
bool setup_wasm_streaming)
|
||||
: isolate_(Initialize(event_loop, setup_wasm_streaming)),
|
||||
isolate_holder_(CreateIsolateHolder(isolate_)),
|
||||
locker_(isolate_) {
|
||||
: isolate_holder_{CreateIsolateHolder(
|
||||
Initialize(event_loop, setup_wasm_streaming))},
|
||||
isolate_{isolate_holder_.isolate()},
|
||||
locker_{isolate_} {
|
||||
isolate_->Enter();
|
||||
|
||||
v8::HandleScope scope(isolate_);
|
||||
@@ -339,12 +340,4 @@ void JavascriptEnvironment::DestroyMicrotasksRunner() {
|
||||
base::CurrentThread::Get()->RemoveTaskObserver(microtasks_runner_.get());
|
||||
}
|
||||
|
||||
NodeEnvironment::NodeEnvironment(node::Environment* env) : env_(env) {}
|
||||
|
||||
NodeEnvironment::~NodeEnvironment() {
|
||||
auto* isolate_data = env_->isolate_data();
|
||||
node::FreeEnvironment(env_);
|
||||
node::FreeIsolateData(isolate_data);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -43,29 +43,17 @@ class JavascriptEnvironment {
|
||||
v8::Isolate* Initialize(uv_loop_t* event_loop, bool setup_wasm_streaming);
|
||||
std::unique_ptr<node::MultiIsolatePlatform> platform_;
|
||||
|
||||
raw_ptr<v8::Isolate> isolate_;
|
||||
gin::IsolateHolder isolate_holder_;
|
||||
|
||||
// owned-by: isolate_holder_
|
||||
const raw_ptr<v8::Isolate> isolate_;
|
||||
|
||||
// depends-on: isolate_
|
||||
v8::Locker locker_;
|
||||
|
||||
std::unique_ptr<MicrotasksRunner> microtasks_runner_;
|
||||
};
|
||||
|
||||
// Manage the Node Environment automatically.
|
||||
class NodeEnvironment {
|
||||
public:
|
||||
explicit NodeEnvironment(node::Environment* env);
|
||||
~NodeEnvironment();
|
||||
|
||||
// disable copy
|
||||
NodeEnvironment(const NodeEnvironment&) = delete;
|
||||
NodeEnvironment& operator=(const NodeEnvironment&) = delete;
|
||||
|
||||
node::Environment* env() { return env_; }
|
||||
|
||||
private:
|
||||
raw_ptr<node::Environment> env_;
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_JAVASCRIPT_ENVIRONMENT_H_
|
||||
|
||||
@@ -55,25 +55,27 @@ void NativeBrowserViewMac::SetBounds(const gfx::Rect& bounds) {
|
||||
auto* iwc_view = GetInspectableWebContentsView();
|
||||
if (!iwc_view)
|
||||
return;
|
||||
|
||||
auto* view = iwc_view->GetNativeView().GetNativeNSView();
|
||||
auto* superview = view.superview;
|
||||
const auto superview_height = superview ? superview.frame.size.height : 0;
|
||||
view.frame =
|
||||
NSMakeRect(bounds.x(), superview_height - bounds.y() - bounds.height(),
|
||||
bounds.width(), bounds.height());
|
||||
const auto superview_height =
|
||||
view.superview ? view.superview.frame.size.height : 0;
|
||||
int y_coord = superview_height - bounds.y() - bounds.height();
|
||||
|
||||
view.frame = NSMakeRect(bounds.x(), y_coord, bounds.width(), bounds.height());
|
||||
}
|
||||
|
||||
gfx::Rect NativeBrowserViewMac::GetBounds() {
|
||||
auto* iwc_view = GetInspectableWebContentsView();
|
||||
if (!iwc_view)
|
||||
return gfx::Rect();
|
||||
|
||||
NSView* view = iwc_view->GetNativeView().GetNativeNSView();
|
||||
const int superview_height =
|
||||
(view.superview) ? view.superview.frame.size.height : 0;
|
||||
return gfx::Rect(
|
||||
view.frame.origin.x,
|
||||
superview_height - view.frame.origin.y - view.frame.size.height,
|
||||
view.frame.size.width, view.frame.size.height);
|
||||
view.superview ? view.superview.frame.size.height : 0;
|
||||
int y_coord = superview_height - view.frame.origin.y - view.frame.size.height;
|
||||
|
||||
return gfx::Rect(view.frame.origin.x, y_coord, view.frame.size.width,
|
||||
view.frame.size.height);
|
||||
}
|
||||
|
||||
void NativeBrowserViewMac::SetBackgroundColor(SkColor color) {
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
#include "shell/browser/native_window_features.h"
|
||||
|
||||
namespace features {
|
||||
const base::Feature kWaylandWindowDecorations{
|
||||
"WaylandWindowDecorations", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
const base::Feature kWaylandWindowDecorations{"WaylandWindowDecorations",
|
||||
base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
}
|
||||
|
||||
@@ -834,23 +834,19 @@ void NativeWindowMac::SetResizable(bool resizable) {
|
||||
ScopedDisableResize disable_resize;
|
||||
SetStyleMask(resizable, NSWindowStyleMaskResizable);
|
||||
|
||||
bool was_fullscreenable = IsFullScreenable();
|
||||
|
||||
// Right now, resizable and fullscreenable are decoupled in
|
||||
// documentation and on Windows/Linux. Chromium disables
|
||||
// fullscreenability if resizability is false on macOS as well
|
||||
// as disabling the maximize traffic light unless the window
|
||||
// is both resizable and maximizable. To work around this, we want
|
||||
// to match behavior on other platforms by disabiliting the maximize
|
||||
// button but keeping fullscreenability enabled.
|
||||
// TODO(codebytere): refactor this once we have a better solution.
|
||||
// fullscreen collection behavior as well as the maximize traffic
|
||||
// light in SetCanResize if resizability is false on macOS unless
|
||||
// the window is both resizable and maximizable. We want consistent
|
||||
// cross-platform behavior, so if resizability is disabled we disable
|
||||
// the maximize button and ensure fullscreenability matches user setting.
|
||||
SetCanResize(resizable);
|
||||
if (!resizable) {
|
||||
SetFullScreenable(true);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton] setEnabled:false];
|
||||
} else {
|
||||
SetFullScreenable(true);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton]
|
||||
setEnabled:IsFullScreenable()];
|
||||
}
|
||||
SetFullScreenable(was_fullscreenable);
|
||||
[[window_ standardWindowButton:NSWindowZoomButton]
|
||||
setEnabled:resizable ? was_fullscreenable : false];
|
||||
}
|
||||
|
||||
bool NativeWindowMac::IsResizable() {
|
||||
|
||||
@@ -44,11 +44,13 @@
|
||||
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"
|
||||
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/net/asar/asar_url_loader_factory.h"
|
||||
#include "shell/browser/protocol_registry.h"
|
||||
#include "shell/browser/ui/inspectable_web_contents_delegate.h"
|
||||
#include "shell/browser/ui/inspectable_web_contents_view.h"
|
||||
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/platform_util.h"
|
||||
#include "third_party/blink/public/common/logging/logging_utils.h"
|
||||
#include "third_party/blink/public/common/page/page_zoom.h"
|
||||
@@ -585,6 +587,23 @@ void InspectableWebContents::LoadCompleted() {
|
||||
prefs.FindString("currentDockState");
|
||||
base::RemoveChars(*current_dock_state, "\"", &dock_state_);
|
||||
}
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
auto* api_web_contents = api::WebContents::From(GetWebContents());
|
||||
if (api_web_contents) {
|
||||
auto* win =
|
||||
static_cast<NativeWindowViews*>(api_web_contents->owner_window());
|
||||
// When WCO is enabled, undock the devtools if the current dock
|
||||
// position overlaps with the position of window controls to avoid
|
||||
// broken layout.
|
||||
if (win && win->IsWindowControlsOverlayEnabled()) {
|
||||
if (IsAppRTL() && dock_state_ == "left") {
|
||||
dock_state_ = "undocked";
|
||||
} else if (dock_state_ == "right") {
|
||||
dock_state_ = "undocked";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::u16string javascript = base::UTF8ToUTF16(
|
||||
"UI.DockController.instance().setDockSide(\"" + dock_state_ + "\");");
|
||||
GetDevToolsWebContents()->GetPrimaryMainFrame()->ExecuteJavaScript(
|
||||
|
||||
@@ -150,7 +150,13 @@ void ClientFrameViewLinux::Init(NativeWindowViews* window,
|
||||
}
|
||||
|
||||
gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
|
||||
return frame_provider_->GetFrameThicknessDip();
|
||||
const auto insets = frame_provider_->GetFrameThicknessDip();
|
||||
// We shouldn't draw frame decorations for the tiled edges.
|
||||
// See https://wayland.app/protocols/xdg-shell#xdg_toplevel:enum:state
|
||||
return gfx::Insets::TLBR(tiled_edges().top ? 0 : insets.top(),
|
||||
tiled_edges().left ? 0 : insets.left(),
|
||||
tiled_edges().bottom ? 0 : insets.bottom(),
|
||||
tiled_edges().right ? 0 : insets.right());
|
||||
}
|
||||
|
||||
gfx::Insets ClientFrameViewLinux::GetInputInsets() const {
|
||||
|
||||
@@ -45,7 +45,7 @@ electron::UsbChooserContext* GetChooserContext(
|
||||
// These extensions can claim the smart card USB class and automatically gain
|
||||
// permissions for devices that have an interface with this class.
|
||||
constexpr auto kSmartCardPrivilegedExtensionIds =
|
||||
base::MakeFixedFlatSet<base::StringPiece>({
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
// Smart Card Connector Extension and its Beta version, see
|
||||
// crbug.com/1233881.
|
||||
"khpfeaanjngmcnplbdlpegiifgpfgdco",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#include "shell/common/application_info.h"
|
||||
|
||||
#include "base/i18n/rtl.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "chrome/browser/browser_process.h"
|
||||
#include "chrome/common/chrome_version.h"
|
||||
#include "content/public/common/user_agent.h"
|
||||
#include "electron/electron_version.h"
|
||||
@@ -47,4 +49,11 @@ std::string GetApplicationUserAgent() {
|
||||
return content::BuildUserAgentFromProduct(user_agent);
|
||||
}
|
||||
|
||||
bool IsAppRTL() {
|
||||
const std::string& locale = g_browser_process->GetApplicationLocale();
|
||||
base::i18n::TextDirection text_direction =
|
||||
base::i18n::GetTextDirectionForLocaleInStartUp(locale.c_str());
|
||||
return text_direction == base::i18n::RIGHT_TO_LEFT;
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -25,6 +25,8 @@ std::string GetApplicationVersion();
|
||||
// Returns the user agent of Electron.
|
||||
std::string GetApplicationUserAgent();
|
||||
|
||||
bool IsAppRTL();
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
PCWSTR GetRawAppUserModelID();
|
||||
bool GetAppUserModelID(ScopedHString* app_id);
|
||||
|
||||
@@ -39,6 +39,7 @@ generated_json_strings("generated_api_json_strings") {
|
||||
sources = [
|
||||
"extension.json",
|
||||
"resources_private.idl",
|
||||
"scripting.idl",
|
||||
"tabs.json",
|
||||
]
|
||||
|
||||
@@ -59,6 +60,7 @@ generated_json_strings("generated_api_json_strings") {
|
||||
generated_types("generated_api_types") {
|
||||
sources = [
|
||||
"resources_private.idl",
|
||||
"scripting.idl",
|
||||
"tabs.json",
|
||||
]
|
||||
|
||||
|
||||
@@ -51,5 +51,9 @@
|
||||
"matches": [
|
||||
"chrome://print/*"
|
||||
]
|
||||
}]
|
||||
}],
|
||||
"scripting": {
|
||||
"dependencies": ["permission:scripting"],
|
||||
"contexts": ["blessed_extension"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
// well as feature.h, simple_feature.h, and feature_provider.h.
|
||||
|
||||
{
|
||||
"author": {
|
||||
"channel": "stable",
|
||||
"extension_types": "all"
|
||||
},
|
||||
"content_scripts": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension"]
|
||||
@@ -15,8 +19,17 @@
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension"]
|
||||
},
|
||||
"host_permissions": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension"],
|
||||
"min_manifest_version": 3
|
||||
},
|
||||
"minimum_chrome_version": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension"]
|
||||
},
|
||||
"short_name": {
|
||||
"channel": "stable",
|
||||
"extension_types": "all"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,18 @@
|
||||
"extension_types": [
|
||||
"extension"
|
||||
]
|
||||
},
|
||||
"tabs": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
"extension"
|
||||
]
|
||||
},
|
||||
"scripting": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
"extension"
|
||||
],
|
||||
"min_manifest_version": 3
|
||||
}
|
||||
}
|
||||
262
shell/common/extensions/api/scripting.idl
Normal file
262
shell/common/extensions/api/scripting.idl
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright 2020 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Use the <code>chrome.scripting</code> API to execute script in different
|
||||
// contexts.
|
||||
namespace scripting {
|
||||
callback InjectedFunction = void();
|
||||
|
||||
// The origin for a style change.
|
||||
// See <a href="https://developer.mozilla.org/en-US/docs/Glossary/Style_origin">style origins</a>
|
||||
// for more info.
|
||||
enum StyleOrigin {
|
||||
AUTHOR,
|
||||
USER
|
||||
};
|
||||
|
||||
// The JavaScript world for a script to execute within.
|
||||
enum ExecutionWorld {
|
||||
// Specifies the isolated world, which is the execution environment unique
|
||||
// to this extension.
|
||||
ISOLATED,
|
||||
// Specifies the main world of the DOM, which is the execution environment
|
||||
// shared with the host page's JavaScript.
|
||||
MAIN
|
||||
};
|
||||
|
||||
dictionary InjectionTarget {
|
||||
// The ID of the tab into which to inject.
|
||||
long tabId;
|
||||
|
||||
// The <a href="https://developer.chrome.com/extensions/webNavigation#frame_ids">IDs</a>
|
||||
// of specific frames to inject into.
|
||||
long[]? frameIds;
|
||||
|
||||
// The <a href="https://developer.chrome.com/extensions/webNavigation#document_ids">IDs</a>
|
||||
// of specific documentIds to inject into. This must not be set if
|
||||
// <code>frameIds</code> is set.
|
||||
DOMString[]? documentIds;
|
||||
|
||||
// Whether the script should inject into all frames within the tab. Defaults
|
||||
// to false.
|
||||
// This must not be true if <code>frameIds</code> is specified.
|
||||
boolean? allFrames;
|
||||
};
|
||||
|
||||
dictionary ScriptInjection {
|
||||
// A JavaScript function to inject. This function will be serialized, and
|
||||
// then deserialized for injection. This means that any bound parameters
|
||||
// and execution context will be lost.
|
||||
// Exactly one of <code>files</code> and <code>func</code> must be
|
||||
// specified.
|
||||
[serializableFunction]InjectedFunction? func;
|
||||
|
||||
// The arguments to curry into a provided function. This is only valid if
|
||||
// the <code>func</code> parameter is specified. These arguments must be
|
||||
// JSON-serializable.
|
||||
any[]? args;
|
||||
|
||||
// We used to call the injected function `function`, but this is
|
||||
// incompatible with JavaScript's object declaration shorthand (see
|
||||
// https://crbug.com/1166438). We leave this silently in for backwards
|
||||
// compatibility.
|
||||
// TODO(devlin): Remove this in M95.
|
||||
[nodoc, serializableFunction]InjectedFunction? function;
|
||||
|
||||
// The path of the JS or CSS files to inject, relative to the extension's
|
||||
// root directory.
|
||||
// Exactly one of <code>files</code> and <code>func</code> must be
|
||||
// specified.
|
||||
DOMString[]? files;
|
||||
|
||||
// Details specifying the target into which to inject the script.
|
||||
InjectionTarget target;
|
||||
|
||||
// The JavaScript "world" to run the script in. Defaults to
|
||||
// <code>ISOLATED</code>.
|
||||
ExecutionWorld? world;
|
||||
|
||||
// Whether the injection should be triggered in the target as soon as
|
||||
// possible. Note that this is not a guarantee that injection will occur
|
||||
// prior to page load, as the page may have already loaded by the time the
|
||||
// script reaches the target.
|
||||
boolean? injectImmediately;
|
||||
};
|
||||
|
||||
dictionary CSSInjection {
|
||||
// Details specifying the target into which to insert the CSS.
|
||||
InjectionTarget target;
|
||||
|
||||
// A string containing the CSS to inject.
|
||||
// Exactly one of <code>files</code> and <code>css</code> must be
|
||||
// specified.
|
||||
DOMString? css;
|
||||
|
||||
// The path of the CSS files to inject, relative to the extension's root
|
||||
// directory.
|
||||
// Exactly one of <code>files</code> and <code>css</code> must be
|
||||
// specified.
|
||||
DOMString[]? files;
|
||||
|
||||
// The style origin for the injection. Defaults to <code>'AUTHOR'</code>.
|
||||
StyleOrigin? origin;
|
||||
};
|
||||
|
||||
dictionary InjectionResult {
|
||||
// The result of the script execution.
|
||||
any? result;
|
||||
|
||||
// The frame associated with the injection.
|
||||
long frameId;
|
||||
|
||||
// The document associated with the injection.
|
||||
DOMString documentId;
|
||||
};
|
||||
|
||||
// Describes a content script to be injected into a web page registered
|
||||
// through this API.
|
||||
dictionary RegisteredContentScript {
|
||||
// The id of the content script, specified in the API call. Must not start
|
||||
// with a '_' as it's reserved as a prefix for generated script IDs.
|
||||
DOMString id;
|
||||
// Specifies which pages this content script will be injected into. See
|
||||
// <a href="match_patterns">Match Patterns</a> for more details on the
|
||||
// syntax of these strings. Must be specified for
|
||||
// $(ref:registerContentScripts).
|
||||
DOMString[]? matches;
|
||||
// Excludes pages that this content script would otherwise be injected into.
|
||||
// See <a href="match_patterns">Match Patterns</a> for more details on the
|
||||
// syntax of these strings.
|
||||
DOMString[]? excludeMatches;
|
||||
// The list of CSS files to be injected into matching pages. These are
|
||||
// injected in the order they appear in this array, before any DOM is
|
||||
// constructed or displayed for the page.
|
||||
DOMString[]? css;
|
||||
// The list of JavaScript files to be injected into matching pages. These
|
||||
// are injected in the order they appear in this array.
|
||||
DOMString[]? js;
|
||||
// If specified true, it will inject into all frames, even if the frame is
|
||||
// not the top-most frame in the tab. Each frame is checked independently
|
||||
// for URL requirements; it will not inject into child frames if the URL
|
||||
// requirements are not met. Defaults to false, meaning that only the top
|
||||
// frame is matched.
|
||||
boolean? allFrames;
|
||||
// TODO(devlin): Add documentation once the implementation is complete. See
|
||||
// crbug.com/55084.
|
||||
[nodoc]
|
||||
boolean? matchOriginAsFallback;
|
||||
// Specifies when JavaScript files are injected into the web page. The
|
||||
// preferred and default value is <code>document_idle</code>.
|
||||
extensionTypes.RunAt? runAt;
|
||||
// Specifies if this content script will persist into future sessions. The
|
||||
// default is true.
|
||||
boolean? persistAcrossSessions;
|
||||
// The JavaScript "world" to run the script in. Defaults to
|
||||
// <code>ISOLATED</code>.
|
||||
ExecutionWorld? world;
|
||||
};
|
||||
|
||||
// An object used to filter content scripts for
|
||||
// ${ref:getRegisteredContentScripts}.
|
||||
dictionary ContentScriptFilter {
|
||||
// If specified, $(ref:getRegisteredContentScripts) will only return scripts
|
||||
// with an id specified in this list.
|
||||
DOMString[]? ids;
|
||||
};
|
||||
|
||||
callback ScriptInjectionCallback = void(InjectionResult[] results);
|
||||
|
||||
callback CSSInjectionCallback = void();
|
||||
|
||||
callback RegisterContentScriptsCallback = void();
|
||||
|
||||
callback GetRegisteredContentScriptsCallback = void(
|
||||
RegisteredContentScript[] scripts);
|
||||
|
||||
callback UnregisterContentScriptsCallback = void();
|
||||
|
||||
callback UpdateContentScriptsCallback = void();
|
||||
|
||||
interface Properties {
|
||||
// An object available for content scripts running in isolated worlds to use
|
||||
// and modify as a JS object. One instance exists per frame and is shared
|
||||
// between all content scripts for a given extension. This object is
|
||||
// initialized when the frame is created, before document_start.
|
||||
// TODO(crbug.com/1054624): Enable this once implementation is complete.
|
||||
[nodoc, nocompile] static long globalParams();
|
||||
};
|
||||
|
||||
interface Functions {
|
||||
// Injects a script into a target context. The script will be run at
|
||||
// <code>document_idle</code>. If the script evaluates to a promise,
|
||||
// the browser will wait for the promise to settle and return the
|
||||
// resulting value.
|
||||
// |injection|: The details of the script which to inject.
|
||||
// |callback|: Invoked upon completion of the injection. The resulting
|
||||
// array contains the result of execution for each frame where the
|
||||
// injection succeeded.
|
||||
[supportsPromises] static void executeScript(
|
||||
ScriptInjection injection,
|
||||
optional ScriptInjectionCallback callback);
|
||||
|
||||
// Inserts a CSS stylesheet into a target context.
|
||||
// If multiple frames are specified, unsuccessful injections are ignored.
|
||||
// |injection|: The details of the styles to insert.
|
||||
// |callback|: Invoked upon completion of the insertion.
|
||||
[supportsPromises] static void insertCSS(
|
||||
CSSInjection injection,
|
||||
optional CSSInjectionCallback callback);
|
||||
|
||||
// Removes a CSS stylesheet that was previously inserted by this extension
|
||||
// from a target context.
|
||||
// |injection|: The details of the styles to remove. Note that the
|
||||
// <code>css</code>, <code>files</code>, and <code>origin</code> properties
|
||||
// must exactly match the stylesheet inserted through $(ref:insertCSS).
|
||||
// Attempting to remove a non-existent stylesheet is a no-op.
|
||||
// |callback|: A callback to be invoked upon the completion of the removal.
|
||||
[supportsPromises] static void removeCSS(
|
||||
CSSInjection injection,
|
||||
optional CSSInjectionCallback callback);
|
||||
|
||||
// Registers one or more content scripts for this extension.
|
||||
// |scripts|: Contains a list of scripts to be registered. If there are
|
||||
// errors during script parsing/file validation, or if the IDs specified
|
||||
// already exist, then no scripts are registered.
|
||||
// |callback|: A callback to be invoked once scripts have been fully
|
||||
// registered or if an error has occurred.
|
||||
[supportsPromises] static void registerContentScripts(
|
||||
RegisteredContentScript[] scripts,
|
||||
optional RegisterContentScriptsCallback callback);
|
||||
|
||||
// Returns all dynamically registered content scripts for this extension
|
||||
// that match the given filter.
|
||||
// |filter|: An object to filter the extension's dynamically registered
|
||||
// scripts.
|
||||
[supportsPromises] static void getRegisteredContentScripts(
|
||||
optional ContentScriptFilter filter,
|
||||
GetRegisteredContentScriptsCallback callback);
|
||||
|
||||
// Unregisters content scripts for this extension.
|
||||
// |filter|: If specified, only unregisters dynamic content scripts which
|
||||
// match the filter. Otherwise, all of the extension's dynamic content
|
||||
// scripts are unregistered.
|
||||
// |callback|: A callback to be invoked once scripts have been unregistered
|
||||
// or if an error has occurred.
|
||||
[supportsPromises] static void unregisterContentScripts(
|
||||
optional ContentScriptFilter filter,
|
||||
optional UnregisterContentScriptsCallback callback);
|
||||
|
||||
// Updates one or more content scripts for this extension.
|
||||
// |scripts|: Contains a list of scripts to be updated. A property is only
|
||||
// updated for the existing script if it is specified in this object. If
|
||||
// there are errors during script parsing/file validation, or if the IDs
|
||||
// specified do not correspond to a fully registered script, then no scripts
|
||||
// are updated.
|
||||
// |callback|: A callback to be invoked once scripts have been updated or
|
||||
// if an error has occurred.
|
||||
[supportsPromises] static void updateContentScripts(
|
||||
RegisteredContentScript[] scripts,
|
||||
optional RegisterContentScriptsCallback callback);
|
||||
};
|
||||
};
|
||||
@@ -704,9 +704,362 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
// Electron does not yet support tab events - we define them here because otherwise extensions crash when
|
||||
// they try to listen for them.
|
||||
"events": [
|
||||
{
|
||||
"name": "onCreated",
|
||||
"deprecated": "chrome.tabs.onCreated is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is created. Note that the tab's URL and tab group membership may not be set at the time this event is fired, but you can listen to onUpdated events so as to be notified when a URL is set or the tab is added to a tab group.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Tab",
|
||||
"name": "tab",
|
||||
"description": "Details of the tab that was created."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onUpdated",
|
||||
"deprecated": "chrome.tabs.onUpdated is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is updated.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "changeInfo",
|
||||
"description": "Lists the changes to the state of the tab that was updated.",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "TabStatus",
|
||||
"optional": true,
|
||||
"description": "The tab's loading status."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The tab's URL if it has changed."
|
||||
},
|
||||
"groupId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": -1,
|
||||
"description": "The tab's new group."
|
||||
},
|
||||
"pinned": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "The tab's new pinned state."
|
||||
},
|
||||
"audible": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "The tab's new audible state."
|
||||
},
|
||||
"discarded": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "The tab's new discarded state."
|
||||
},
|
||||
"autoDiscardable": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "The tab's new auto-discardable state."
|
||||
},
|
||||
"mutedInfo": {
|
||||
"$ref": "MutedInfo",
|
||||
"optional": true,
|
||||
"description": "The tab's new muted state and the reason for the change."
|
||||
},
|
||||
"favIconUrl": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The tab's new favicon URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The tab's new title."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "Tab",
|
||||
"name": "tab",
|
||||
"description": "Gives the state of the tab that was updated."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onMoved",
|
||||
"deprecated": "chrome.tabs.onMoved is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is moved within a window. Only one move event is fired, representing the tab the user directly moved. Move events are not fired for the other tabs that must move in response to the manually-moved tab. This event is not fired when a tab is moved between windows; for details, see $(ref:tabs.onDetached).",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "moveInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"fromIndex": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"toIndex": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onSelectionChanged",
|
||||
"deprecated": "chrome.tabs.onSelectionChanged is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fires when the selected tab in a window changes.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the tab that has become active."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "selectInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the window the selected tab changed inside of."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onActiveChanged",
|
||||
"deprecated": "chrome.tabs.onActiveChanged is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fires when the selected tab in a window changes. Note that the tab's URL may not be set at the time this event fired, but you can listen to $(ref:tabs.onUpdated) events so as to be notified when a URL is set.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the tab that has become active."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "selectInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the window the selected tab changed inside of."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onActivated",
|
||||
"deprecated": "chrome.tabs.onActivated is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fires when the active tab in a window changes. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events so as to be notified when a URL is set.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "activeInfo",
|
||||
"properties": {
|
||||
"tabId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the tab that has become active."
|
||||
},
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the window the active tab changed inside of."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onHighlightChanged",
|
||||
"deprecated": "chrome.tabs.onHighlightChanged is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when the highlighted or selected tabs in a window changes.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "selectInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The window whose tabs changed."
|
||||
},
|
||||
"tabIds": {
|
||||
"type": "array",
|
||||
"name": "tabIds",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"description": "All highlighted tabs in the window."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onHighlighted",
|
||||
"deprecated": "chrome.tabs.onHighlighted is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when the highlighted or selected tabs in a window changes.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "highlightInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The window whose tabs changed."
|
||||
},
|
||||
"tabIds": {
|
||||
"type": "array",
|
||||
"name": "tabIds",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"description": "All highlighted tabs in the window."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onDetached",
|
||||
"deprecated": "chrome.tabs.onDetached is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is detached from a window; for example, because it was moved between windows.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "detachInfo",
|
||||
"properties": {
|
||||
"oldWindowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"oldPosition": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onAttached",
|
||||
"deprecated": "chrome.tabs.onAttached is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is attached to a window; for example, because it was moved between windows.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "attachInfo",
|
||||
"properties": {
|
||||
"newWindowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"newPosition": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onRemoved",
|
||||
"deprecated": "chrome.tabs.onRemoved is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is closed.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "removeInfo",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The window whose tab is closed."
|
||||
},
|
||||
"isWindowClosing": {
|
||||
"type": "boolean",
|
||||
"description": "True when the tab was closed because its parent window was closed."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onReplaced",
|
||||
"deprecated": "chrome.tabs.onReplaced is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is replaced with another tab due to prerendering or instant.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "addedTabId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "removedTabId",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onZoomChange",
|
||||
"deprecated": "chrome.tabs.onZoomChange is not current supported in Electron",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab is zoomed.",
|
||||
"parameters": [
|
||||
|
||||
@@ -39,6 +39,10 @@ constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
|
||||
{mojom::APIPermissionID::kPdfViewerPrivate, "pdfViewerPrivate"},
|
||||
#endif
|
||||
{mojom::APIPermissionID::kManagement, "management"},
|
||||
{mojom::APIPermissionID::kTab, "tabs",
|
||||
APIPermissionInfo::kFlagRequiresManagementUIWarning},
|
||||
{mojom::APIPermissionID::kScripting, "scripting",
|
||||
APIPermissionInfo::kFlagRequiresManagementUIWarning},
|
||||
};
|
||||
base::span<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
|
||||
return base::make_span(permissions_to_register);
|
||||
|
||||
@@ -232,7 +232,7 @@ void ErrorMessageListener(v8::Local<v8::Message> message,
|
||||
// If node CLI inspect support is disabled, allow no debug options.
|
||||
bool IsAllowedOption(base::StringPiece option) {
|
||||
static constexpr auto debug_options =
|
||||
base::MakeFixedFlatSet<base::StringPiece>({
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--debug",
|
||||
"--debug-brk",
|
||||
"--debug-port",
|
||||
@@ -244,13 +244,14 @@ bool IsAllowedOption(base::StringPiece option) {
|
||||
});
|
||||
|
||||
// This should be aligned with what's possible to set via the process object.
|
||||
static constexpr auto options = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--trace-warnings",
|
||||
"--trace-deprecation",
|
||||
"--throw-deprecation",
|
||||
"--no-deprecation",
|
||||
"--dns-result-order",
|
||||
});
|
||||
static constexpr auto options =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--dns-result-order",
|
||||
"--no-deprecation",
|
||||
"--throw-deprecation",
|
||||
"--trace-deprecation",
|
||||
"--trace-warnings",
|
||||
});
|
||||
|
||||
if (debug_options.contains(option))
|
||||
return electron::fuses::IsNodeCliInspectEnabled();
|
||||
@@ -262,14 +263,21 @@ bool IsAllowedOption(base::StringPiece option) {
|
||||
// See https://nodejs.org/api/cli.html#cli_node_options_options
|
||||
void SetNodeOptions(base::Environment* env) {
|
||||
// Options that are unilaterally disallowed
|
||||
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>(
|
||||
{"--enable-fips", "--force-fips", "--openssl-config", "--use-bundled-ca",
|
||||
"--use-openssl-ca", "--experimental-policy"});
|
||||
static constexpr auto disallowed =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--enable-fips",
|
||||
"--experimental-policy",
|
||||
"--force-fips",
|
||||
"--openssl-config",
|
||||
"--use-bundled-ca",
|
||||
"--use-openssl-ca",
|
||||
});
|
||||
|
||||
static constexpr auto pkg_opts = base::MakeFixedFlatSet<base::StringPiece>({
|
||||
"--http-parser",
|
||||
"--max-http-header-size",
|
||||
});
|
||||
static constexpr auto pkg_opts =
|
||||
base::MakeFixedFlatSetSorted<base::StringPiece>({
|
||||
"--http-parser",
|
||||
"--max-http-header-size",
|
||||
});
|
||||
|
||||
if (env->HasVar("NODE_OPTIONS")) {
|
||||
if (electron::fuses::IsNodeOptionsEnabled()) {
|
||||
@@ -469,7 +477,7 @@ void NodeBindings::Initialize(v8::Local<v8::Context> context) {
|
||||
g_is_initialized = true;
|
||||
}
|
||||
|
||||
node::Environment* NodeBindings::CreateEnvironment(
|
||||
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
@@ -636,10 +644,25 @@ node::Environment* NodeBindings::CreateEnvironment(
|
||||
base::PathService::Get(content::CHILD_PROCESS_EXE, &helper_exec_path);
|
||||
process.Set("helperExecPath", helper_exec_path);
|
||||
|
||||
return env;
|
||||
auto env_deleter = [isolate, isolate_data,
|
||||
context = v8::Global<v8::Context>{isolate, context}](
|
||||
node::Environment* nenv) mutable {
|
||||
// When `isolate_data` was created above, a pointer to it was kept
|
||||
// in context's embedder_data[kElectronContextEmbedderDataIndex].
|
||||
// Since we're about to free `isolate_data`, clear that entry
|
||||
v8::HandleScope handle_scope{isolate};
|
||||
context.Get(isolate)->SetAlignedPointerInEmbedderData(
|
||||
kElectronContextEmbedderDataIndex, nullptr);
|
||||
context.Reset();
|
||||
|
||||
node::FreeEnvironment(nenv);
|
||||
node::FreeIsolateData(isolate_data);
|
||||
};
|
||||
|
||||
return {env, std::move(env_deleter)};
|
||||
}
|
||||
|
||||
node::Environment* NodeBindings::CreateEnvironment(
|
||||
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
#define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
@@ -25,12 +26,6 @@ class SingleThreadTaskRunner;
|
||||
|
||||
namespace electron {
|
||||
|
||||
// Choose a reasonable unique index that's higher than any Blink uses
|
||||
// and thus unlikely to collide with an existing index.
|
||||
static constexpr int kElectronContextEmbedderDataIndex =
|
||||
static_cast<int>(gin::kPerContextDataStartIndex) +
|
||||
static_cast<int>(gin::kEmbedderElectron);
|
||||
|
||||
// A helper class to manage uv_handle_t types, e.g. uv_async_t.
|
||||
//
|
||||
// As per the uv docs: "uv_close() MUST be called on each handle before
|
||||
@@ -95,12 +90,15 @@ class NodeBindings {
|
||||
std::vector<std::string> ParseNodeCliFlags();
|
||||
|
||||
// Create the environment and load node.js.
|
||||
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
std::vector<std::string> exec_args);
|
||||
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform);
|
||||
std::shared_ptr<node::Environment> CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform,
|
||||
std::vector<std::string> args,
|
||||
std::vector<std::string> exec_args);
|
||||
|
||||
std::shared_ptr<node::Environment> CreateEnvironment(
|
||||
v8::Handle<v8::Context> context,
|
||||
node::MultiIsolatePlatform* platform);
|
||||
|
||||
// Load node.js in the environment.
|
||||
void LoadEnvironment(node::Environment* env);
|
||||
@@ -111,12 +109,6 @@ class NodeBindings {
|
||||
// Notify embed thread to start polling after environment is loaded.
|
||||
void StartPolling();
|
||||
|
||||
// Clears the PerIsolateData.
|
||||
void clear_isolate_data(v8::Local<v8::Context> context) {
|
||||
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
node::IsolateData* isolate_data(v8::Local<v8::Context> context) const {
|
||||
if (context->GetNumberOfEmbedderDataFields() <=
|
||||
kElectronContextEmbedderDataIndex) {
|
||||
@@ -167,6 +159,12 @@ class NodeBindings {
|
||||
raw_ptr<uv_loop_t> uv_loop_;
|
||||
|
||||
private:
|
||||
// Choose a reasonable unique index that's higher than any Blink uses
|
||||
// and thus unlikely to collide with an existing index.
|
||||
static constexpr int kElectronContextEmbedderDataIndex =
|
||||
static_cast<int>(gin::kPerContextDataStartIndex) +
|
||||
static_cast<int>(gin::kEmbedderElectron);
|
||||
|
||||
// Thread to poll uv events.
|
||||
static void EmbedThreadRunner(void* arg);
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ void ElectronRendererClient::DidCreateScriptContext(
|
||||
v8::Maybe<bool> initialized = node::InitializeContext(renderer_context);
|
||||
CHECK(!initialized.IsNothing() && initialized.FromJust());
|
||||
|
||||
node::Environment* env =
|
||||
std::shared_ptr<node::Environment> env =
|
||||
node_bindings_->CreateEnvironment(renderer_context, nullptr);
|
||||
|
||||
// If we have disabled the site instance overrides we should prevent loading
|
||||
@@ -106,11 +106,11 @@ void ElectronRendererClient::DidCreateScriptContext(
|
||||
BindProcess(env->isolate(), &process_dict, render_frame);
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(env.get());
|
||||
|
||||
if (node_bindings_->uv_env() == nullptr) {
|
||||
// Make uv loop being wrapped by window context.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(env.get());
|
||||
|
||||
// Give the node loop a run to make sure everything is ready.
|
||||
node_bindings_->StartPolling();
|
||||
@@ -124,7 +124,9 @@ void ElectronRendererClient::WillReleaseScriptContext(
|
||||
return;
|
||||
|
||||
node::Environment* env = node::Environment::GetCurrent(context);
|
||||
if (environments_.erase(env) == 0)
|
||||
const auto iter = base::ranges::find_if(
|
||||
environments_, [env](auto& item) { return env == item.get(); });
|
||||
if (iter == environments_.end())
|
||||
return;
|
||||
|
||||
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
|
||||
@@ -143,9 +145,7 @@ void ElectronRendererClient::WillReleaseScriptContext(
|
||||
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
|
||||
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
|
||||
|
||||
node::FreeEnvironment(env);
|
||||
node::FreeIsolateData(node_bindings_->isolate_data(context));
|
||||
node_bindings_->clear_isolate_data(context);
|
||||
environments_.erase(iter);
|
||||
|
||||
microtask_queue->set_microtasks_policy(old_policy);
|
||||
|
||||
@@ -201,7 +201,11 @@ node::Environment* ElectronRendererClient::GetEnvironment(
|
||||
auto context =
|
||||
GetContext(render_frame->GetWebFrame(), v8::Isolate::GetCurrent());
|
||||
node::Environment* env = node::Environment::GetCurrent(context);
|
||||
return base::Contains(environments_, env) ? env : nullptr;
|
||||
|
||||
return base::Contains(environments_, env,
|
||||
[](auto const& item) { return item.get(); })
|
||||
? env
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -55,7 +55,7 @@ class ElectronRendererClient : public RendererClientBase {
|
||||
// The node::Environment::GetCurrent API does not return nullptr when it
|
||||
// is called for a context without node::Environment, so we have to keep
|
||||
// a book of the environments created.
|
||||
std::set<node::Environment*> environments_;
|
||||
std::set<std::shared_ptr<node::Environment>> environments_;
|
||||
|
||||
// Getting main script context from web frame would lazily initializes
|
||||
// its script context. Doing so in a web page without scripts would trigger
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/containers/cxx20_erase_set.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/threading/thread_local.h"
|
||||
#include "shell/common/api/electron_bindings.h"
|
||||
#include "shell/common/gin_helper/event_emitter_caller.h"
|
||||
@@ -61,20 +63,23 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation(
|
||||
// Setup node environment for each window.
|
||||
v8::Maybe<bool> initialized = node::InitializeContext(worker_context);
|
||||
CHECK(!initialized.IsNothing() && initialized.FromJust());
|
||||
node::Environment* env =
|
||||
std::shared_ptr<node::Environment> env =
|
||||
node_bindings_->CreateEnvironment(worker_context, nullptr);
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(env->isolate(), env->process_object());
|
||||
|
||||
// Load everything.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(env.get());
|
||||
|
||||
// Make uv loop being wrapped by window context.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(env.get());
|
||||
|
||||
// Give the node loop a run to make sure everything is ready.
|
||||
node_bindings_->StartPolling();
|
||||
|
||||
// Keep the environment alive until we free it in ContextWillDestroy()
|
||||
environments_.insert(std::move(env));
|
||||
}
|
||||
|
||||
void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
|
||||
@@ -91,9 +96,8 @@ void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
|
||||
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
|
||||
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
|
||||
|
||||
node::FreeEnvironment(env);
|
||||
node::FreeIsolateData(node_bindings_->isolate_data(context));
|
||||
node_bindings_->clear_isolate_data(context);
|
||||
base::EraseIf(environments_,
|
||||
[env](auto const& item) { return item.get() == env; });
|
||||
|
||||
microtask_queue->set_microtasks_policy(old_policy);
|
||||
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
#define ELECTRON_SHELL_RENDERER_WEB_WORKER_OBSERVER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
} // namespace node
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronBindings;
|
||||
@@ -35,6 +42,7 @@ class WebWorkerObserver {
|
||||
private:
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::set<std::shared_ptr<node::Environment>> environments_;
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -30,9 +30,9 @@ NodeService::NodeService(
|
||||
|
||||
NodeService::~NodeService() {
|
||||
if (!node_env_stopped_) {
|
||||
node_env_->env()->set_trace_sync_io(false);
|
||||
node_env_->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,12 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
#endif
|
||||
|
||||
// Create the global environment.
|
||||
node::Environment* env = node_bindings_->CreateEnvironment(
|
||||
node_env_ = node_bindings_->CreateEnvironment(
|
||||
js_env_->isolate()->GetCurrentContext(), js_env_->platform(),
|
||||
params->args, params->exec_args);
|
||||
node_env_ = std::make_unique<NodeEnvironment>(env);
|
||||
|
||||
node::SetProcessExitHandler(
|
||||
env, [this](node::Environment* env, int exit_code) {
|
||||
node_env_.get(), [this](node::Environment* env, int exit_code) {
|
||||
// Destroy node platform.
|
||||
env->set_trace_sync_io(false);
|
||||
js_env_->DestroyMicrotasksRunner();
|
||||
@@ -72,20 +71,21 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
receiver_.ResetWithReason(exit_code, "");
|
||||
});
|
||||
|
||||
env->set_trace_sync_io(env->options()->trace_sync_io);
|
||||
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(env->isolate(), env->process_object());
|
||||
electron_bindings_->BindTo(node_env_->isolate(), node_env_->process_object());
|
||||
|
||||
// Add entry script to process object.
|
||||
gin_helper::Dictionary process(env->isolate(), env->process_object());
|
||||
gin_helper::Dictionary process(node_env_->isolate(),
|
||||
node_env_->process_object());
|
||||
process.SetHidden("_serviceStartupScript", params->script);
|
||||
|
||||
// Setup microtask runner.
|
||||
js_env_->CreateMicrotasksRunner();
|
||||
|
||||
// Wrap the uv loop with global env.
|
||||
node_bindings_->set_uv_env(env);
|
||||
node_bindings_->set_uv_env(node_env_.get());
|
||||
|
||||
// LoadEnvironment should be called after setting up
|
||||
// JavaScriptEnvironment including the microtask runner
|
||||
@@ -94,7 +94,7 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
|
||||
// the exit handler set above will be triggered and it expects
|
||||
// both Node Env and JavaScriptEnviroment are setup to perform
|
||||
// a clean shutdown of this process.
|
||||
node_bindings_->LoadEnvironment(env);
|
||||
node_bindings_->LoadEnvironment(node_env_.get());
|
||||
|
||||
// Run entry script.
|
||||
node_bindings_->PrepareEmbedThread();
|
||||
|
||||
@@ -11,12 +11,17 @@
|
||||
#include "mojo/public/cpp/bindings/receiver.h"
|
||||
#include "shell/services/node/public/mojom/node_service.mojom.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
} // namespace node
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronBindings;
|
||||
class JavascriptEnvironment;
|
||||
class NodeBindings;
|
||||
class NodeEnvironment;
|
||||
|
||||
class NodeService : public node::mojom::NodeService {
|
||||
public:
|
||||
@@ -35,7 +40,7 @@ class NodeService : public node::mojom::NodeService {
|
||||
std::unique_ptr<JavascriptEnvironment> js_env_;
|
||||
std::unique_ptr<NodeBindings> node_bindings_;
|
||||
std::unique_ptr<ElectronBindings> electron_bindings_;
|
||||
std::unique_ptr<NodeEnvironment> node_env_;
|
||||
std::shared_ptr<node::Environment> node_env_;
|
||||
mojo::Receiver<node::mojom::NodeService> receiver_{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -147,6 +147,41 @@ describe('BrowserView module', () => {
|
||||
view.setBounds({} as any);
|
||||
}).to.throw(/conversion failure/);
|
||||
});
|
||||
|
||||
it('can set bounds after view is added to window', () => {
|
||||
view = new BrowserView();
|
||||
|
||||
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
||||
|
||||
w.addBrowserView(view);
|
||||
view.setBounds(bounds);
|
||||
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('can set bounds before view is added to window', () => {
|
||||
view = new BrowserView();
|
||||
|
||||
const bounds = { x: 0, y: 0, width: 50, height: 50 };
|
||||
|
||||
view.setBounds(bounds);
|
||||
w.addBrowserView(view);
|
||||
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('can update bounds', () => {
|
||||
view = new BrowserView();
|
||||
w.addBrowserView(view);
|
||||
|
||||
const bounds1 = { x: 0, y: 0, width: 50, height: 50 };
|
||||
view.setBounds(bounds1);
|
||||
expect(view.getBounds()).to.deep.equal(bounds1);
|
||||
|
||||
const bounds2 = { x: 0, y: 150, width: 50, height: 50 };
|
||||
view.setBounds(bounds2);
|
||||
expect(view.getBounds()).to.deep.equal(bounds2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserView.getBounds()', () => {
|
||||
@@ -156,6 +191,16 @@ describe('BrowserView module', () => {
|
||||
view.setBounds(bounds);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
|
||||
it('does not changer after being added to a window', () => {
|
||||
view = new BrowserView();
|
||||
const bounds = { x: 10, y: 20, width: 30, height: 40 };
|
||||
view.setBounds(bounds);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
|
||||
w.addBrowserView(view);
|
||||
expect(view.getBounds()).to.deep.equal(bounds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserWindow.setBrowserView()', () => {
|
||||
|
||||
@@ -5456,6 +5456,42 @@ describe('BrowserWindow module', () => {
|
||||
expect(w.isFullScreenable()).to.be.true('isFullScreenable');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not open non-fullscreenable child windows in fullscreen if parent is fullscreen', async () => {
|
||||
const w = new BrowserWindow();
|
||||
|
||||
const enterFS = once(w, 'enter-full-screen');
|
||||
w.setFullScreen(true);
|
||||
await enterFS;
|
||||
|
||||
const child = new BrowserWindow({ parent: w, resizable: false, fullscreenable: false });
|
||||
const shown = once(child, 'show');
|
||||
await shown;
|
||||
|
||||
expect(child.resizable).to.be.false('resizable');
|
||||
expect(child.fullScreen).to.be.false('fullscreen');
|
||||
expect(child.fullScreenable).to.be.false('fullscreenable');
|
||||
});
|
||||
|
||||
it('is set correctly with different resizable values', async () => {
|
||||
const w1 = new BrowserWindow({
|
||||
resizable: false,
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
const w2 = new BrowserWindow({
|
||||
resizable: true,
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
const w3 = new BrowserWindow({
|
||||
fullscreenable: false
|
||||
});
|
||||
|
||||
expect(w1.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
expect(w2.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
expect(w3.isFullScreenable()).to.be.false('isFullScreenable');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform === 'darwin')('isHiddenInMissionControl state', () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import * as path from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { ifdescribe } from './lib/spec-helpers';
|
||||
|
||||
// FIXME: The tests are skipped on arm/arm64 and ia32.
|
||||
ifdescribe(!(['arm', 'arm64', 'ia32'].includes(process.arch)))('contentTracing', () => {
|
||||
// FIXME: The tests are skipped on linux arm/arm64
|
||||
ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== 'linux'))('contentTracing', () => {
|
||||
const record = async (options: TraceConfig | TraceCategoriesAndOptions, outputFilePath: string | undefined, recordTimeInMilliseconds = 1e1) => {
|
||||
await app.whenReady();
|
||||
|
||||
@@ -120,9 +120,7 @@ ifdescribe(!(['arm', 'arm64', 'ia32'].includes(process.arch)))('contentTracing',
|
||||
|
||||
describe('captured events', () => {
|
||||
it('include V8 samples from the main process', async function () {
|
||||
// This test is flaky on macOS CI.
|
||||
this.retries(3);
|
||||
|
||||
this.timeout(60000);
|
||||
await contentTracing.startRecording({
|
||||
categoryFilter: 'disabled-by-default-v8.cpu_profiler',
|
||||
traceOptions: 'record-until-full'
|
||||
@@ -131,7 +129,7 @@ ifdescribe(!(['arm', 'arm64', 'ia32'].includes(process.arch)))('contentTracing',
|
||||
const start = Date.now();
|
||||
let n = 0;
|
||||
const f = () => {};
|
||||
while (Date.now() - start < 200 || n < 500) {
|
||||
while (Date.now() - start < 200 && n < 500) {
|
||||
await setTimeout(0);
|
||||
f();
|
||||
n++;
|
||||
|
||||
@@ -283,6 +283,29 @@ describe('setDisplayMediaRequestHandler', () => {
|
||||
expect(ok).to.be.true(message);
|
||||
});
|
||||
|
||||
it('returns a MediaStream with BrowserCaptureMediaStreamTrack when the current tab is selected', async () => {
|
||||
const ses = session.fromPartition('' + Math.random());
|
||||
let requestHandlerCalled = false;
|
||||
ses.setDisplayMediaRequestHandler((request, callback) => {
|
||||
requestHandlerCalled = true;
|
||||
callback({ video: w.webContents.mainFrame });
|
||||
});
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
|
||||
await w.loadURL(serverUrl);
|
||||
const { ok, message } = await w.webContents.executeJavaScript(`
|
||||
navigator.mediaDevices.getDisplayMedia({
|
||||
preferCurrentTab: true,
|
||||
video: true,
|
||||
audio: false,
|
||||
}).then(stream => {
|
||||
const [videoTrack] = stream.getVideoTracks();
|
||||
return { ok: videoTrack instanceof BrowserCaptureMediaStreamTrack, message: null };
|
||||
}, e => ({ok: false, message: e.message}))
|
||||
`, true);
|
||||
expect(requestHandlerCalled).to.be.true();
|
||||
expect(ok).to.be.true(message);
|
||||
});
|
||||
|
||||
ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('can supply a screen response to preferCurrentTab', async () => {
|
||||
const ses = session.fromPartition('' + Math.random());
|
||||
let requestHandlerCalled = false;
|
||||
|
||||
@@ -827,277 +827,383 @@ describe('session module', () => {
|
||||
fs.unlinkSync(downloadFilePath);
|
||||
};
|
||||
|
||||
it('can download using session.downloadURL', (done) => {
|
||||
session.defaultSession.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
session.defaultSession.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can download using session.downloadURL with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when session.downloadURL is called with invalid headers', () => {
|
||||
expect(() => {
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('can download using session.downloadURL with an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
|
||||
it('can download using WebContents.downloadURL', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can download from custom protocols using WebContents.downloadURL', (done) => {
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const handler = (ignoredError: any, callback: Function) => {
|
||||
callback({ url: `${url}:${port}` });
|
||||
};
|
||||
protocol.registerHttpProtocol(protocolName, handler);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item, true);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${protocolName}://item`);
|
||||
});
|
||||
|
||||
it('can download using WebView.downloadURL', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
await w.loadURL('about:blank');
|
||||
function webviewDownload ({ fixtures, url, port }: {fixtures: string, url: string, port: string}) {
|
||||
const webview = new (window as any).WebView();
|
||||
webview.addEventListener('did-finish-load', () => {
|
||||
webview.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
webview.src = `file://${fixtures}/api/blank.html`;
|
||||
document.body.appendChild(webview);
|
||||
}
|
||||
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
describe('session.downloadURL', () => {
|
||||
it('can perform a download', (done) => {
|
||||
session.defaultSession.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
resolve([state, item]);
|
||||
});
|
||||
});
|
||||
});
|
||||
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
|
||||
const [state, item] = await done;
|
||||
assertDownload(state, item);
|
||||
});
|
||||
|
||||
it('can cancel download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('cancelled');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
|
||||
it('can generate a default filename', function (done) {
|
||||
if (process.env.APPVEYOR === 'True') {
|
||||
// FIXME(alexeykuzmin): Skip the test.
|
||||
// this.skip()
|
||||
return done();
|
||||
}
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getFilename()).to.equal('download.pdf');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
|
||||
});
|
||||
|
||||
it('can set options for the save dialog', (done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
|
||||
const options = {
|
||||
window: null,
|
||||
title: 'title',
|
||||
message: 'message',
|
||||
buttonLabel: 'buttonLabel',
|
||||
nameFieldLabel: 'nameFieldLabel',
|
||||
defaultPath: '/',
|
||||
filters: [{
|
||||
name: '1', extensions: ['.1', '.2']
|
||||
}, {
|
||||
name: '2', extensions: ['.3', '.4', '.5']
|
||||
}],
|
||||
showsTagField: true,
|
||||
securityScopedBookmarks: true
|
||||
};
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.setSavePath(filePath);
|
||||
item.setSaveDialogOptions(options);
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getSaveDialogOptions()).to.deep.equal(options);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
describe('when a save path is specified and the URL is unavailable', () => {
|
||||
it('does not display a save dialog and reports the done state as interrupted', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
if (item.getState() === 'interrupted') {
|
||||
item.resume();
|
||||
}
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('interrupted');
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
|
||||
session.defaultSession.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can perform a download with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when called with invalid headers', () => {
|
||||
expect(() => {
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('correctly handles a download with an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
session.defaultSession.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('webContents.downloadURL', () => {
|
||||
it('can perform a download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
it('can perform a download with a valid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
w.webContents.session.once('will-download', (e, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', () => {
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic i-am-an-auth-header'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadDone;
|
||||
expect(item.getState()).to.equal('completed');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
});
|
||||
|
||||
it('throws when called with invalid headers', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
expect(() => {
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
// @ts-ignore this line is intentionally incorrect
|
||||
headers: 'i-am-a-bad-header'
|
||||
});
|
||||
}).to.throw(/Invalid value for headers - must be an object/);
|
||||
});
|
||||
|
||||
it('correctly handles a download and an invalid auth header', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': mockPDF.length,
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
|
||||
});
|
||||
res.end(mockPDF);
|
||||
}
|
||||
});
|
||||
|
||||
const { port } = await listen(server);
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
|
||||
w.webContents.session.once('will-download', (_, item) => {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', (e, state) => {
|
||||
console.log(state);
|
||||
try {
|
||||
resolve(item);
|
||||
} catch { }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
w.webContents.downloadURL(`${url}:${port}`, {
|
||||
headers: {
|
||||
Authorization: 'wtf-is-this'
|
||||
}
|
||||
});
|
||||
|
||||
const item = await downloadFailed;
|
||||
expect(item.getState()).to.equal('interrupted');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(0);
|
||||
});
|
||||
|
||||
it('can download from custom protocols', (done) => {
|
||||
const protocol = session.defaultSession.protocol;
|
||||
const handler = (ignoredError: any, callback: Function) => {
|
||||
callback({ url: `${url}:${port}` });
|
||||
};
|
||||
protocol.registerHttpProtocol(protocolName, handler);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
assertDownload(state, item, true);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`${protocolName}://item`);
|
||||
});
|
||||
|
||||
it('can cancel download', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('cancelled');
|
||||
expect(item.getFilename()).to.equal('mock.pdf');
|
||||
expect(item.getMimeType()).to.equal('application/pdf');
|
||||
expect(item.getReceivedBytes()).to.equal(0);
|
||||
expect(item.getTotalBytes()).to.equal(mockPDF.length);
|
||||
expect(item.getContentDisposition()).to.equal(contentDisposition);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
|
||||
it('can generate a default filename', function (done) {
|
||||
if (process.env.APPVEYOR === 'True') {
|
||||
// FIXME(alexeykuzmin): Skip the test.
|
||||
// this.skip()
|
||||
return done();
|
||||
}
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getFilename()).to.equal('download.pdf');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
|
||||
});
|
||||
|
||||
it('can set options for the save dialog', (done) => {
|
||||
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
|
||||
const options = {
|
||||
window: null,
|
||||
title: 'title',
|
||||
message: 'message',
|
||||
buttonLabel: 'buttonLabel',
|
||||
nameFieldLabel: 'nameFieldLabel',
|
||||
defaultPath: '/',
|
||||
filters: [{
|
||||
name: '1', extensions: ['.1', '.2']
|
||||
}, {
|
||||
name: '2', extensions: ['.3', '.4', '.5']
|
||||
}],
|
||||
showsTagField: true,
|
||||
securityScopedBookmarks: true
|
||||
};
|
||||
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.setSavePath(filePath);
|
||||
item.setSaveDialogOptions(options);
|
||||
item.on('done', function () {
|
||||
try {
|
||||
expect(item.getSaveDialogOptions()).to.deep.equal(options);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
item.cancel();
|
||||
});
|
||||
w.webContents.downloadURL(`${url}:${port}`);
|
||||
});
|
||||
|
||||
describe('when a save path is specified and the URL is unavailable', () => {
|
||||
it('does not display a save dialog and reports the done state as interrupted', (done) => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
if (item.getState() === 'interrupted') {
|
||||
item.resume();
|
||||
}
|
||||
item.on('done', function (e, state) {
|
||||
try {
|
||||
expect(state).to.equal('interrupted');
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebView.downloadURL', () => {
|
||||
it('can perform a download', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
|
||||
await w.loadURL('about:blank');
|
||||
function webviewDownload ({ fixtures, url, port }: { fixtures: string, url: string, port: string }) {
|
||||
const webview = new (window as any).WebView();
|
||||
webview.addEventListener('did-finish-load', () => {
|
||||
webview.downloadURL(`${url}:${port}/`);
|
||||
});
|
||||
webview.src = `file://${fixtures}/api/blank.html`;
|
||||
document.body.appendChild(webview);
|
||||
}
|
||||
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
|
||||
w.webContents.session.once('will-download', function (e, item) {
|
||||
item.savePath = downloadFilePath;
|
||||
item.on('done', function (e, state) {
|
||||
resolve([state, item]);
|
||||
});
|
||||
});
|
||||
});
|
||||
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
|
||||
const [state, item] = await done;
|
||||
assertDownload(state, item);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,6 +375,16 @@ describe('webContents module', () => {
|
||||
await expect(w.loadURL(w.getURL() + '#foo')).to.eventually.be.fulfilled();
|
||||
});
|
||||
|
||||
it('resolves after browser initiated navigation', async () => {
|
||||
let finishedLoading = false;
|
||||
w.webContents.on('did-finish-load', function () {
|
||||
finishedLoading = true;
|
||||
});
|
||||
|
||||
await w.loadFile(path.join(fixturesPath, 'pages', 'navigate_in_page_and_wait.html'));
|
||||
expect(finishedLoading).to.be.true();
|
||||
});
|
||||
|
||||
it('rejects when failing to load a file URL', async () => {
|
||||
await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected()
|
||||
.and.have.property('code', 'ERR_FILE_NOT_FOUND');
|
||||
@@ -1905,16 +1915,6 @@ describe('webContents module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(features.isPrintingEnabled())('getPrinters()', () => {
|
||||
afterEach(closeAllWindows);
|
||||
it('can get printer list', async () => {
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
|
||||
await w.loadURL('about:blank');
|
||||
const printers = w.webContents.getPrinters();
|
||||
expect(printers).to.be.an('array');
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(features.isPrintingEnabled())('getPrintersAsync()', () => {
|
||||
afterEach(closeAllWindows);
|
||||
it('can get printer list', async () => {
|
||||
|
||||
@@ -2050,10 +2050,32 @@ describe('chromium features', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://accessibility', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL('chrome://accessibility');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
expect(pageExists).to.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://gpu', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL('chrome://gpu');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
expect(pageExists).to.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome://media-internals', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL('chrome://media-internals');
|
||||
await w.loadURL('chrome://media-internals');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
@@ -2064,7 +2086,7 @@ describe('chromium features', () => {
|
||||
describe('chrome://webrtc-internals', () => {
|
||||
it('loads the page successfully', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.loadURL('chrome://webrtc-internals');
|
||||
await w.loadURL('chrome://webrtc-internals');
|
||||
const pageExists = await w.webContents.executeJavaScript(
|
||||
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
|
||||
);
|
||||
|
||||
@@ -69,6 +69,67 @@ describe('chrome extensions', () => {
|
||||
`)).to.eventually.have.property('id');
|
||||
});
|
||||
|
||||
describe('host_permissions', async () => {
|
||||
let customSession: Session;
|
||||
let w: BrowserWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: customSession,
|
||||
sandbox: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('recognize malformed host permissions', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const extPath = path.join(fixtures, 'extensions', 'host-permissions', 'malformed');
|
||||
customSession.loadExtension(extPath);
|
||||
|
||||
const warning = await new Promise(resolve => { process.on('warning', resolve); });
|
||||
|
||||
const malformedHost = /Permission 'malformed_host' is unknown or URL pattern is malformed/;
|
||||
|
||||
expect(warning).to.match(malformedHost);
|
||||
});
|
||||
|
||||
it('can grant special privileges to urls with host permissions', async () => {
|
||||
const extPath = path.join(fixtures, 'extensions', 'host-permissions', 'privileged-tab-info');
|
||||
await customSession.loadExtension(extPath);
|
||||
|
||||
await w.loadURL(url);
|
||||
|
||||
const message = { method: 'query' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response).to.have.lengthOf(1);
|
||||
|
||||
const tab = response[0];
|
||||
expect(tab).to.have.property('url').that.is.a('string');
|
||||
expect(tab).to.have.property('title').that.is.a('string');
|
||||
expect(tab).to.have.property('active').that.is.a('boolean');
|
||||
expect(tab).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(tab).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(tab).to.have.property('groupId').that.is.a('number');
|
||||
expect(tab).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(tab).to.have.property('id').that.is.a('number');
|
||||
expect(tab).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(tab).to.have.property('index').that.is.a('number');
|
||||
expect(tab).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(tab).to.have.property('selected').that.is.a('boolean');
|
||||
expect(tab).to.have.property('windowId').that.is.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('supports minimum_chrome_version manifest key', async () => {
|
||||
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
|
||||
const w = new BrowserWindow({
|
||||
@@ -81,7 +142,7 @@ describe('chrome extensions', () => {
|
||||
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const extPath = path.join(fixtures, 'extensions', 'chrome-too-low-version');
|
||||
const extPath = path.join(fixtures, 'extensions', 'minimum-chrome-version');
|
||||
const load = customSession.loadExtension(extPath);
|
||||
await expect(load).to.eventually.be.rejectedWith(
|
||||
`Loading extension at ${extPath} failed with: This extension requires Chromium version 999 or greater.`
|
||||
@@ -842,15 +903,14 @@ describe('chrome extensions', () => {
|
||||
|
||||
before(async () => {
|
||||
customSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await customSession.loadExtension(path.join(fixtures, 'extensions', 'tabs-api-async'));
|
||||
await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: customSession,
|
||||
nodeIntegration: true
|
||||
session: customSession
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -913,27 +973,55 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('get', async () => {
|
||||
await w.loadURL(url);
|
||||
describe('get', () => {
|
||||
it('returns tab properties', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const message = { method: 'get' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
const message = { method: 'get' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('title').that.is.a('string');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
});
|
||||
|
||||
it('does not return privileged properties without tabs permission', async () => {
|
||||
const noPrivilegeSes = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await noPrivilegeSes.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges'));
|
||||
|
||||
w = new BrowserWindow({ show: false, webPreferences: { session: noPrivilegeSes } });
|
||||
await w.loadURL(url);
|
||||
|
||||
w.webContents.executeJavaScript('window.postMessage(\'{}\', \'*\')');
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).not.to.have.property('url');
|
||||
expect(response).not.to.have.property('title');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('reload', async () => {
|
||||
@@ -960,6 +1048,19 @@ describe('chrome extensions', () => {
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response).to.have.property('url').that.is.a('string');
|
||||
expect(response).to.have.property('title').that.is.a('string');
|
||||
expect(response).to.have.property('active').that.is.a('boolean');
|
||||
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
|
||||
expect(response).to.have.property('discarded').that.is.a('boolean');
|
||||
expect(response).to.have.property('groupId').that.is.a('number');
|
||||
expect(response).to.have.property('highlighted').that.is.a('boolean');
|
||||
expect(response).to.have.property('id').that.is.a('number');
|
||||
expect(response).to.have.property('incognito').that.is.a('boolean');
|
||||
expect(response).to.have.property('index').that.is.a('number');
|
||||
expect(response).to.have.property('pinned').that.is.a('boolean');
|
||||
expect(response).to.have.property('selected').that.is.a('boolean');
|
||||
expect(response).to.have.property('windowId').that.is.a('number');
|
||||
expect(response).to.have.property('mutedInfo').that.is.a('object');
|
||||
const { mutedInfo } = response;
|
||||
expect(mutedInfo).to.deep.eq({
|
||||
@@ -1028,5 +1129,75 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.scripting', () => {
|
||||
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-scripting'));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: customSession,
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('executeScript', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const message = { method: 'executeScript' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const updated = await once(w.webContents, 'page-title-updated');
|
||||
expect(updated[1]).to.equal('HEY HEY HEY');
|
||||
});
|
||||
|
||||
it('registerContentScripts', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const message = { method: 'registerContentScripts' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.be.an('array').with.lengthOf(1);
|
||||
expect(response[0]).to.deep.equal({
|
||||
allFrames: false,
|
||||
id: 'session-script',
|
||||
js: ['content.js'],
|
||||
matchOriginAsFallback: false,
|
||||
matches: ['<all_urls>'],
|
||||
persistAcrossSessions: false,
|
||||
runAt: 'document_start',
|
||||
world: 'ISOLATED'
|
||||
});
|
||||
});
|
||||
|
||||
it('insertCSS', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
const bgBefore = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).backgroundColor');
|
||||
expect(bgBefore).to.equal('rgba(0, 0, 0, 0)');
|
||||
|
||||
const message = { method: 'insertCSS' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response.success).to.be.true();
|
||||
|
||||
const bgAfter = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).backgroundColor');
|
||||
expect(bgAfter).to.equal('rgb(255, 0, 0)');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
51
spec/fixtures/extensions/chrome-scripting/background.js
vendored
Normal file
51
spec/fixtures/extensions/chrome-scripting/background.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
/* global chrome */
|
||||
|
||||
const handleRequest = async (request, sender, sendResponse) => {
|
||||
const { method } = request;
|
||||
const tabId = sender.tab.id;
|
||||
|
||||
switch (method) {
|
||||
case 'executeScript': {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
function: () => {
|
||||
document.title = 'HEY HEY HEY';
|
||||
return document.title;
|
||||
}
|
||||
}).then(() => {
|
||||
console.log('success');
|
||||
}).catch((err) => {
|
||||
console.log('error', err);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'registerContentScripts': {
|
||||
await chrome.scripting.registerContentScripts([{
|
||||
id: 'session-script',
|
||||
js: ['content.js'],
|
||||
persistAcrossSessions: false,
|
||||
matches: ['<all_urls>'],
|
||||
runAt: 'document_start'
|
||||
}]);
|
||||
|
||||
chrome.scripting.getRegisteredContentScripts().then(sendResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'insertCSS': {
|
||||
chrome.scripting.insertCSS({
|
||||
target: { tabId },
|
||||
css: 'body { background-color: red; }'
|
||||
}).then(() => {
|
||||
sendResponse({ success: true });
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
handleRequest(request, sender, sendResponse);
|
||||
return true;
|
||||
});
|
||||
0
spec/fixtures/extensions/chrome-scripting/content.js
vendored
Normal file
0
spec/fixtures/extensions/chrome-scripting/content.js
vendored
Normal file
30
spec/fixtures/extensions/chrome-scripting/main.js
vendored
Normal file
30
spec/fixtures/extensions/chrome-scripting/main.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/* global chrome */
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
sendResponse(request);
|
||||
});
|
||||
|
||||
const map = {
|
||||
executeScript () {
|
||||
chrome.runtime.sendMessage({ method: 'executeScript' }, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
},
|
||||
registerContentScripts () {
|
||||
chrome.runtime.sendMessage({ method: 'registerContentScripts' }, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
},
|
||||
insertCSS () {
|
||||
chrome.runtime.sendMessage({ method: 'insertCSS' }, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const dispatchTest = (event) => {
|
||||
const { method, args = [] } = JSON.parse(event.data);
|
||||
map[method](...args);
|
||||
};
|
||||
|
||||
window.addEventListener('message', dispatchTest, false);
|
||||
17
spec/fixtures/extensions/chrome-scripting/manifest.json
vendored
Normal file
17
spec/fixtures/extensions/chrome-scripting/manifest.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "execute-script",
|
||||
"version": "1.0",
|
||||
"permissions": [
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"content_scripts": [{
|
||||
"matches": [ "<all_urls>"],
|
||||
"js": ["main.js"],
|
||||
"run_at": "document_start"
|
||||
}],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "tabs-api-async",
|
||||
"name": "api-async",
|
||||
"version": "1.0",
|
||||
"content_scripts": [
|
||||
{
|
||||
@@ -8,6 +8,7 @@
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"permissions": ["tabs"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
6
spec/fixtures/extensions/chrome-tabs/no-privileges/background.js
vendored
Normal file
6
spec/fixtures/extensions/chrome-tabs/no-privileges/background.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/* global chrome */
|
||||
|
||||
chrome.runtime.onMessage.addListener((_request, sender, sendResponse) => {
|
||||
chrome.tabs.get(sender.tab.id).then(sendResponse);
|
||||
return true;
|
||||
});
|
||||
11
spec/fixtures/extensions/chrome-tabs/no-privileges/main.js
vendored
Normal file
11
spec/fixtures/extensions/chrome-tabs/no-privileges/main.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/* global chrome */
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
sendResponse(request);
|
||||
});
|
||||
|
||||
window.addEventListener('message', () => {
|
||||
chrome.runtime.sendMessage({}, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
}, false);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user