Compare commits

...

25 Commits

Author SHA1 Message Date
Sudowoodo Release Bot
882cdb1f7a Bump v22.0.0-nightly.20220810 2022-08-10 06:01:07 -07:00
Samuel Attard
1d95b98cc8 docs: fix getStoragePath return type (#35288)
Fixes #35255
2022-08-09 22:39:36 -07:00
Milan Burda
8646bf8d30 chore: remove deprecated 'new-window' event (#34526) 2022-08-09 17:57:05 -04:00
Trang Le
32fefb1f50 fix(docs): fix a typo in section on debugging with VSCode (#35256)
fix(docs): fix a typo
2022-08-09 15:01:38 -04:00
Sudowoodo Release Bot
6548808054 Bump v22.0.0-nightly.20220809 2022-08-09 06:01:28 -07:00
Jeremy Rose
faa2f7afa3 test: migrate asar specs to main runner (#35230)
* test: migrate node specs to main

* test: migrate asar specs to main runner

* fix execFile
2022-08-09 09:39:14 +02:00
Darshan Sen
f3dbdaaf33 build: fix error in the ts-compile-doc-change step (#35258)
build: fix error in the ts-compile-doc-change step

Fixes the following error: https://app.circleci.com/pipelines/github/electron/electron/56517/workflows/ea0f6548-e0ac-40c6-bacb-e24610cd6670/jobs/1287168?invite=true#step-103-29

```sh
$ webpack --config build/webpack/webpack.config.asar.js --output-filename=asar.js --output-path=./.tmp --env.mode=development
[webpack-cli] Error: Unknown option '--env.mode=development'
[webpack-cli] Run 'webpack --help' to see available commands and options
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```

This probably started happening because of the recent webpack upgrade
in https://github.com/electron/electron/pull/34990.

Signed-off-by: Darshan Sen <raisinten@gmail.com>
2022-08-08 17:09:09 -04:00
Sudowoodo Release Bot
0400eb2e60 Bump v22.0.0-nightly.20220808 2022-08-08 06:02:50 -07:00
Aaron Meriwether
91f9436ad8 fix: app.relaunch loses args when execPath specified (#35108) 2022-08-08 10:12:06 +02:00
Milan Burda
34b985c556 refactor: use optional chaining / nullish coalescing operator (#35217) 2022-08-08 10:11:04 +02:00
John Kleinschmidt
76431ac1fa test: temporarily disable tests on mas arm64 that are causing a crash (#35226)
* test: temporarily disable test on mas arm64 that is causing crash

* disable the right test

* chore: speculative fix for CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER crash

* enable all the tests

* Revert "chore: speculative fix for CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER crash"

This reverts commit b7c8ef364c.

* test: disable tests that crash on  mas arm64
2022-08-06 19:02:04 -04:00
Shelley Vohr
a11cc3274f build: fix webpack prod failure (#35227) 2022-08-05 09:21:00 -07:00
Sudowoodo Release Bot
a719568ac1 Revert "Bump v22.0.0-nightly.20220805"
This reverts commit fbcd8f8a6e.
2022-08-05 07:54:04 -07:00
Sudowoodo Release Bot
fbcd8f8a6e Bump v22.0.0-nightly.20220805 2022-08-05 06:01:13 -07:00
Jeremy Rose
aaa60dc0bc test: migrate remaining chromium specs to main (#35216) 2022-08-04 17:20:56 -07:00
Jeremy Rose
4cfdef0ffd test: migrate node specs to main (#35212) 2022-08-04 17:20:17 -07:00
Erick Zhao
b9fea0d2d2 docs: remove reference to electron/i18n in README (#35228) 2022-08-04 15:18:06 -07:00
Sudowoodo Release Bot
1b2e5b4106 Revert "Bump v22.0.0-nightly.20220804"
This reverts commit 47a08f9570.
2022-08-04 08:27:56 -07:00
Sudowoodo Release Bot
47a08f9570 Bump v22.0.0-nightly.20220804 2022-08-04 06:00:52 -07:00
Brad Carter
21117ea5b2 docs: update tray docs with info for mac menubar icons (#35136) 2022-08-04 11:24:32 +02:00
Jeremy Rose
6d859dcd7f feat: add WebContents.ipc (#34959) 2022-08-03 16:55:12 -07:00
Jeremy Rose
bba22ae720 test: migrate <webview> tag event specs to main runner (#35077) 2022-08-03 12:03:44 -07:00
Jeremy Rose
d15348ecc2 test: migrate webview attribute specs to spec-main (#35076) 2022-08-03 09:59:00 -07:00
Shelley Vohr
3baf713648 build: upgrade webpack and related deps (#34990) 2022-08-03 10:42:50 -04:00
Sudowoodo Release Bot
2b96d06960 Revert "Bump v21.0.0-nightly.20220803"
This reverts commit 4e919c919c.
2022-08-03 07:36:56 -07:00
52 changed files with 4321 additions and 6320 deletions

View File

@@ -990,7 +990,7 @@ step-ts-compile: &step-ts-compile
do
out="${f:29}"
if [ "$out" != "base.js" ]; then
node script/yarn webpack --config $f --output-filename=$out --output-path=./.tmp --env.mode=development
node script/yarn webpack --config $f --output-filename=$out --output-path=./.tmp --env mode=development
fi
done

View File

@@ -1 +1 @@
21.0.0-nightly.20220803
22.0.0-nightly.20220810

View File

@@ -5,7 +5,7 @@
[![Electron Discord Invite](https://img.shields.io/discord/745037351163527189?color=%237289DA&label=chat&logo=discord&logoColor=white)](https://discord.gg/electronjs)
:memo: Available Translations: 🇨🇳 🇧🇷 🇪🇸 🇯🇵 🇷🇺 🇫🇷 🇺🇸 🇩🇪.
View these docs in other languages at [electron/i18n](https://github.com/electron/i18n/tree/master/content/).
View these docs in other languages on our [Crowdin](https://crowdin.com/project/electron) project.
The Electron framework lets you write cross-platform desktop applications
using JavaScript, HTML and CSS. It is based on [Node.js](https://nodejs.org/) and

View File

@@ -75,9 +75,17 @@ module.exports = ({
if (targetDeletesNodeGlobals) {
plugins.push(new webpack.ProvidePlugin({
process: ['@electron/internal/common/webpack-provider', 'process'],
Buffer: ['@electron/internal/common/webpack-provider', 'Buffer'],
global: ['@electron/internal/common/webpack-provider', '_global'],
Buffer: ['@electron/internal/common/webpack-provider', 'Buffer']
process: ['@electron/internal/common/webpack-provider', 'process']
}));
}
// Webpack 5 no longer polyfills process or Buffer.
if (!alwaysHasNode) {
plugins.push(new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
}));
}
@@ -129,7 +137,12 @@ if ((globalThis.process || binding.process).argv.includes("--profile-electron-in
// Force timers to resolve to our dependency that doesn't use window.postMessage
timers: path.resolve(electronRoot, 'node_modules', 'timers-browserify', 'main.js')
},
extensions: ['.ts', '.js']
extensions: ['.ts', '.js'],
fallback: {
// We provide our own "timers" import above, any usage of setImmediate inside
// one of our renderer bundles should import it from the 'timers' package
setImmediate: false
}
},
module: {
rules: [{
@@ -150,10 +163,7 @@ if ((globalThis.process || binding.process).argv.includes("--profile-electron-in
},
node: {
__dirname: false,
__filename: false,
// We provide our own "timers" import above, any usage of setImmediate inside
// one of our renderer bundles should import it from the 'timers' package
setImmediate: false
__filename: false
},
optimization: {
minimize: env.mode === 'production',

View File

@@ -30,11 +30,14 @@ template("webpack_build") {
args = [
"--config",
rebase_path(invoker.config_file),
"--output-filename=" + get_path_info(invoker.out_file, "file"),
"--output-path=" + rebase_path(get_path_info(invoker.out_file, "dir")),
"--env.buildflags=" +
rebase_path("$target_gen_dir/buildflags/buildflags.h"),
"--env.mode=" + mode,
"--output-filename",
get_path_info(invoker.out_file, "file"),
"--output-path",
rebase_path(get_path_info(invoker.out_file, "dir")),
"--env",
"buildflags=" + rebase_path("$target_gen_dir/buildflags/buildflags.h"),
"--env",
"mode=" + mode,
]
deps += [ "//electron/buildflags" ]

View File

@@ -1049,7 +1049,7 @@ is emitted.
#### `ses.getStoragePath()`
A `string | null` indicating the absolute file system path where data for this
Returns `string | null` - The absolute file system path where data for this
session is persisted on disk. For in memory sessions this returns `null`.
### Instance Properties

View File

@@ -1,3 +0,0 @@
# NewWindowWebContentsEvent Object extends `Event`
* `newGuest` BrowserWindow (optional)

View File

@@ -25,15 +25,20 @@ app.whenReady().then(() => {
})
```
__Platform limitations:__
__Platform Considerations__
If you want to keep exact same behaviors on all platforms, you should not
rely on the `click` event; instead, always attach a context menu to the tray icon.
__Linux__
* On Linux the app indicator will be used if it is supported, otherwise
`GtkStatusIcon` will be used instead.
* On Linux distributions that only have app indicator support, you have to
install `libappindicator1` to make the tray icon work.
* The app indicator will be used if it is supported, otherwise
`GtkStatusIcon` will be used instead.
* App indicator will only be shown when it has a context menu.
* When app indicator is used on Linux, the `click` event is ignored.
* On Linux in order for changes made to individual `MenuItem`s to take effect,
* The `click` event is ignored when using the app indicator.
* In order for changes made to individual `MenuItem`s to take effect,
you have to call `setContextMenu` again. For example:
```javascript
@@ -55,10 +60,16 @@ app.whenReady().then(() => {
})
```
* On Windows it is recommended to use `ICO` icons to get best visual effects.
__MacOS__
If you want to keep exact same behaviors on all platforms, you should not
rely on the `click` event and always attach a context menu to the tray icon.
* Icons passed to the Tray constructor should be [Template Images](native-image.md#template-image).
* To make sure your icon isn't grainy on retina monitors, be sure your `@2x` image is 144dpi.
* If you are bundling your application (e.g., with webpack for development), be sure that the file names are not being mangled or hashed. The filename needs to end in Template, and the `@2x` image needs to have the same filename as the standard image, or MacOS will not magically invert your image's colors or use the high density image.
* 16x16 (72dpi) and 32x32@2x (144dpi) work well for most icons.
__Windows__
* It is recommended to use `ICO` icons to get best visual effects.
### `new Tray(image, [guid])`

View File

@@ -156,64 +156,6 @@ Returns:
Emitted when page receives favicon urls.
#### Event: 'new-window' _Deprecated_
Returns:
* `event` NewWindowWebContentsEvent
* `url` string
* `frameName` string
* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`,
`new-window`, `save-to-disk` and `other`.
* `options` BrowserWindowConstructorOptions - The options which will be used for creating the new
[`BrowserWindow`](browser-window.md).
* `additionalFeatures` string[] - The non-standard features (features not handled
by Chromium or Electron) given to `window.open()`. Deprecated, and will now
always be the empty array `[]`.
* `referrer` [Referrer](structures/referrer.md) - The referrer that will be
passed to the new window. May or may not result in the `Referer` header being
sent, depending on the referrer policy.
* `postBody` [PostBody](structures/post-body.md) (optional) - The post data that
will be sent to the new window, along with the appropriate headers that will
be set. If no post data is to be sent, the value will be `null`. Only defined
when the window is being created by a form that set `target=_blank`.
Deprecated in favor of [`webContents.setWindowOpenHandler`](web-contents.md#contentssetwindowopenhandlerhandler).
Emitted when the page requests to open a new window for a `url`. It could be
requested by `window.open` or an external link like `<a target='_blank'>`.
By default a new `BrowserWindow` will be created for the `url`.
Calling `event.preventDefault()` will prevent Electron from automatically creating a
new [`BrowserWindow`](browser-window.md). If you call `event.preventDefault()` and manually create a new
[`BrowserWindow`](browser-window.md) then you must set `event.newGuest` to reference the new [`BrowserWindow`](browser-window.md)
instance, failing to do so may result in unexpected behavior. For example:
```javascript
myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures, referrer, postBody) => {
event.preventDefault()
const win = new BrowserWindow({
webContents: options.webContents, // use existing webContents if provided
show: false
})
win.once('ready-to-show', () => win.show())
if (!options.webContents) {
const loadOptions = {
httpReferrer: referrer
}
if (postBody != null) {
const { data, contentType, boundary } = postBody
loadOptions.postData = postBody.data
loadOptions.extraHeaders = `content-type: ${contentType}; boundary=${boundary}`
}
win.loadURL(url, loadOptions) // existing webContents will be navigated automatically
}
event.newGuest = win
})
```
#### Event: 'did-create-window'
Returns:
@@ -862,6 +804,8 @@ Returns:
Emitted when the renderer process sends an asynchronous message via `ipcRenderer.send()`.
See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents.
#### Event: 'ipc-message-sync'
Returns:
@@ -872,6 +816,8 @@ Returns:
Emitted when the renderer process sends a synchronous message via `ipcRenderer.sendSync()`.
See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents.
#### Event: 'preferred-size-changed'
Returns:
@@ -1985,6 +1931,35 @@ This corresponds to the [animationPolicy][] accessibility feature in Chromium.
### Instance Properties
#### `contents.ipc` _Readonly_
An [`IpcMain`](ipc-main.md) scoped to just IPC messages sent from this
WebContents.
IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or
`ipcRenderer.postMessage` will be delivered in the following order:
1. `contents.on('ipc-message')`
2. `contents.mainFrame.on(channel)`
3. `contents.ipc.on(channel)`
4. `ipcMain.on(channel)`
Handlers registered with `invoke` will be checked in the following order. The
first one that is defined will be called, the rest will be ignored.
1. `contents.mainFrame.handle(channel)`
2. `contents.handle(channel)`
3. `ipcMain.handle(channel)`
A handler or event listener registered on the WebContents will receive IPC
messages sent from any frame, including child frames. In most cases, only the
main frame can send IPC messages. However, if the `nodeIntegrationInSubFrames`
option is enabled, it is possible for child frames to send IPC messages also.
In that case, handlers should check the `senderFrame` property of the IPC event
to ensure that the message is coming from the expected frame. Alternatively,
register handlers on the appropriate frame directly using the
[`WebFrameMain.ipc`](web-frame-main.md#frameipc-readonly) interface.
#### `contents.audioMuted`
A `boolean` property that determines whether this page is muted.

View File

@@ -140,6 +140,31 @@ ipcRenderer.on('port', (e, msg) => {
### Instance Properties
#### `frame.ipc` _Readonly_
An [`IpcMain`](ipc-main.md) instance scoped to the frame.
IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or
`ipcRenderer.postMessage` will be delivered in the following order:
1. `contents.on('ipc-message')`
2. `contents.mainFrame.on(channel)`
3. `contents.ipc.on(channel)`
4. `ipcMain.on(channel)`
Handlers registered with `invoke` will be checked in the following order. The
first one that is defined will be called, the rest will be ignored.
1. `contents.mainFrame.handle(channel)`
2. `contents.handle(channel)`
3. `ipcMain.handle(channel)`
In most cases, only the main frame of a WebContents can send or receive IPC
messages. However, if the `nodeIntegrationInSubFrames` option is enabled, it is
possible for child frames to send and receive IPC messages also. The
[`WebContents.ipc`](web-contents.md#contentsipc-readonly) interface may be more
convenient when `nodeIntegrationInSubFrames` is not enabled.
#### `frame.url` _Readonly_
A `string` representing the current URL of the frame.

View File

@@ -805,33 +805,6 @@ const requestId = webview.findInPage('test')
console.log(requestId)
```
### Event: 'new-window'
Returns:
* `url` string
* `frameName` string
* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`,
`new-window`, `save-to-disk` and `other`.
* `options` BrowserWindowConstructorOptions - The options which should be used for creating the new
[`BrowserWindow`](browser-window.md).
Fired when the guest page attempts to open a new browser window.
The following example code opens the new url in system's default browser.
```javascript
const { shell } = require('electron')
const webview = document.querySelector('webview')
webview.addEventListener('new-window', async (e) => {
const protocol = (new URL(e.url)).protocol
if (protocol === 'http:' || protocol === 'https:') {
await shell.openExternal(e.url)
}
})
```
### Event: 'will-navigate'
Returns:

View File

@@ -12,6 +12,24 @@ This document uses the following convention to categorize breaking changes:
* **Deprecated:** An API was marked as deprecated. The API will continue to function, but will emit a deprecation warning, and will be removed in a future release.
* **Removed:** An API or feature was removed, and is no longer supported by Electron.
## Planned Breaking API Changes (22.0)
### Removed: WebContents `new-window` event
The `new-window` event of WebContents has been removed. It is replaced by [`webContents.setWindowOpenHandler()`](api/web-contents.md#contentssetwindowopenhandlerhandler).
```js
// Removed in Electron 21
webContents.on('new-window', (event) => {
event.preventDefault()
})
// Replace with
webContents.setWindowOpenHandler((details) => {
return { action: 'deny' }
})
```
## Planned Breaking API Changes (20.0)
### Behavior Changed: V8 Memory Cage enabled

View File

@@ -350,7 +350,7 @@ app.whenReady().then(() => {
## Optional: Debugging from VS Code
If you want to debug your application using VS Code, you have need attach VS Code to
If you want to debug your application using VS Code, you need to attach VS Code to
both the main and renderer processes. Here is a sample configuration for you to
run. Create a launch.json configuration in a new `.vscode` folder in your project:

View File

@@ -98,7 +98,6 @@ auto_filenames = {
"docs/api/structures/mime-typed-buffer.md",
"docs/api/structures/mouse-input-event.md",
"docs/api/structures/mouse-wheel-input-event.md",
"docs/api/structures/new-window-web-contents-event.md",
"docs/api/structures/notification-action.md",
"docs/api/structures/notification-response.md",
"docs/api/structures/payment-discount.md",

View File

@@ -60,8 +60,8 @@ const splitPath = (archivePathOrBuffer: string | Buffer) => {
// Convert asar archive's Stats object to fs's Stats object.
let nextInode = 0;
const uid = process.getuid != null ? process.getuid() : 0;
const gid = process.getgid != null ? process.getgid() : 0;
const uid = process.getuid?.() ?? 0;
const gid = process.getgid?.() ?? 0;
const fakeTime = new Date();

View File

@@ -68,7 +68,7 @@ const spawnUpdate = function (args: string[], detached: boolean, callback: Funct
if (code !== 0) {
// Disabled for backwards compatibility:
// eslint-disable-next-line standard/no-callback-literal
return callback(`Command failed: ${signal != null ? signal : code}\n${stderr}`);
return callback(`Command failed: ${signal ?? code}\n${stderr}`);
}
// Success.

View File

@@ -9,8 +9,7 @@ let currentlyRunning: {
// |options.types| can't be empty and must be an array
function isValid (options: Electron.SourcesOptions) {
const types = options ? options.types : undefined;
return Array.isArray(types);
return Array.isArray(options?.types);
}
export async function getSources (args: Electron.SourcesOptions) {

View File

@@ -2,7 +2,4 @@ import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
const ipcMain = new IpcMainImpl();
// Do not throw exception when channel name is "error".
ipcMain.on('error', () => {});
export default ipcMain;

View File

@@ -395,7 +395,7 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
this.on('change', changeListener);
const escapeItemListener = (item: Electron.TouchBarItemType | null) => {
window._setEscapeTouchBarItem(item != null ? item : {});
window._setEscapeTouchBarItem(item ?? {});
};
this.on('escape-item-change', escapeItemListener);

View File

@@ -9,12 +9,15 @@ import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
// session is not used here, the purpose is to make sure session is initialized
// before the webContents module.
// eslint-disable-next-line
session
const webFrameMainBinding = process._linkedBinding('electron_browser_web_frame_main');
let nextId = 0;
const getNextId = function () {
return ++nextId;
@@ -556,6 +559,12 @@ WebContents.prototype._init = function () {
this._windowOpenHandler = null;
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', {
get () { return ipc; },
enumerable: true
});
// Dispatch IPC messages to the ipc module.
this.on('-ipc-message' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) {
addSenderFrameToEvent(event);
@@ -564,6 +573,9 @@ WebContents.prototype._init = function () {
} else {
addReplyToEvent(event);
this.emit('ipc-message', event, channel, ...args);
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
}
});
@@ -575,8 +587,10 @@ WebContents.prototype._init = function () {
console.error(`Error occurred in handler for '${channel}':`, error);
event.sendReply({ error: error.toString() });
};
const target = internal ? ipcMainInternal : ipcMain;
if ((target as any)._invokeHandlers.has(channel)) {
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
const targets: (ElectronInternal.IpcMainInternal| undefined)[] = internal ? [ipcMainInternal] : [maybeWebFrame && maybeWebFrame.ipc, ipc, ipcMain];
const target = targets.find(target => target && (target as any)._invokeHandlers.has(channel));
if (target) {
(target as any)._invokeHandlers.get(channel)(event, ...args);
} else {
event._throw(`No handler registered for '${channel}'`);
@@ -590,10 +604,13 @@ WebContents.prototype._init = function () {
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);
if (this.listenerCount('ipc-message-sync') === 0 && ipcMain.listenerCount(channel) === 0) {
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0 && (!maybeWebFrame || maybeWebFrame.ipc.listenerCount(channel) === 0)) {
console.warn(`WebContents #${this.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`);
}
this.emit('ipc-message-sync', event, channel, ...args);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args);
ipc.emit(channel, event, ...args);
ipcMain.emit(channel, event, ...args);
}
});
@@ -601,6 +618,9 @@ WebContents.prototype._init = function () {
this.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent, internal: boolean, channel: string, message: any, ports: any[]) {
addSenderFrameToEvent(event);
event.ports = ports.map(p => new MessagePortMain(p));
ipc.emit(channel, event, message);
const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId);
maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, message);
ipcMain.emit(channel, event, message);
});
@@ -650,7 +670,6 @@ WebContents.prototype._init = function () {
const options = result.browserWindowConstructorOptions;
if (!event.defaultPrevented) {
openGuestWindow({
event,
embedder: event.sender,
disposition,
referrer,
@@ -697,18 +716,16 @@ WebContents.prototype._init = function () {
transparent: windowOpenOverriddenOptions.transparent,
...windowOpenOverriddenOptions.webPreferences
} : undefined;
// TODO(zcbenz): The features string is parsed twice: here where it is
// passed to C++, and in |makeBrowserWindowOptions| later where it is
// not actually used since the WebContents is created here.
// We should be able to remove the latter once the |new-window| event
// is removed.
const { webPreferences: parsedWebPreferences } = parseFeatures(rawFeatures);
// Parameters should keep same with |makeBrowserWindowOptions|.
const webPreferences = makeWebPreferences({
embedder: event.sender,
insecureParsedWebPreferences: parsedWebPreferences,
secureOverrideWebPreferences
});
windowOpenOverriddenOptions = {
...windowOpenOverriddenOptions,
webPreferences
};
this._setNextChildWebPreferences(webPreferences);
}
});
@@ -730,7 +747,6 @@ WebContents.prototype._init = function () {
}
openGuestWindow({
event,
embedder: event.sender,
guest: webContents,
overrideBrowserWindowOptions: overriddenOptions,

View File

@@ -1,7 +1,16 @@
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
const { WebFrameMain, fromId } = process._linkedBinding('electron_browser_web_frame_main');
Object.defineProperty(WebFrameMain.prototype, 'ipc', {
get () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
}
});
WebFrameMain.prototype.send = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument');

View File

@@ -22,13 +22,6 @@ const supportedWebViewEvents = Object.keys(webViewEvents);
const guestInstances = new Map<number, GuestInstance>();
const embedderElementsMap = new Map<string, number>();
function sanitizeOptionsForGuest (options: Record<string, any>) {
const ret = { ...options };
// WebContents values can't be sent over IPC.
delete ret.webContents;
return ret;
}
function makeWebPreferences (embedder: Electron.WebContents, params: Record<string, any>) {
// parse the 'webpreferences' attribute string, if set
// this uses the same parsing rules as window.open uses for its features
@@ -38,8 +31,8 @@ function makeWebPreferences (embedder: Electron.WebContents, params: Record<stri
: null;
const webPreferences: Electron.WebPreferences = {
nodeIntegration: params.nodeintegration != null ? params.nodeintegration : false,
nodeIntegrationInSubFrames: params.nodeintegrationinsubframes != null ? params.nodeintegrationinsubframes : false,
nodeIntegration: params.nodeintegration ?? false,
nodeIntegrationInSubFrames: params.nodeintegrationinsubframes ?? false,
plugins: params.plugins,
zoomFactor: embedder.zoomFactor,
disablePopups: !params.allowpopups,
@@ -156,15 +149,6 @@ const createGuest = function (embedder: Electron.WebContents, embedderFrameId: n
});
}
guest.on('new-window', function (event, url, frameName, disposition, options) {
sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'new-window', {
url,
frameName,
disposition,
options: sanitizeOptionsForGuest(options)
});
});
// Dispatch guest's IPC messages to embedder.
guest.on('ipc-message-host' as any, function (event: Electron.IpcMainEvent, channel: string, args: any[]) {
sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'ipc-message', {

View File

@@ -5,7 +5,7 @@
* out-of-process (cross-origin) are created here. "Embedder" roughly means
* "parent."
*/
import { BrowserWindow, deprecate } from 'electron/main';
import { BrowserWindow } from 'electron/main';
import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
import { parseFeatures } from '@electron/internal/browser/parse-features-string';
@@ -24,13 +24,8 @@ const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name)
/**
* `openGuestWindow` is called to create and setup event handling for the new
* window.
*
* Until its removal in 12.0.0, the `new-window` event is fired, allowing the
* user to preventDefault() on the passed event (which ends up calling
* DestroyWebContents).
*/
export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
event: { sender: WebContents, defaultPrevented: boolean },
export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
embedder: WebContents,
guest?: WebContents,
referrer: Referrer,
@@ -41,23 +36,14 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
outlivesOpener: boolean,
}): BrowserWindow | undefined {
const { url, frameName, features } = windowOpenArgs;
const browserWindowOptions = makeBrowserWindowOptions({
embedder,
features,
overrideOptions: overrideBrowserWindowOptions
});
const didCancelEvent = emitDeprecatedNewWindowEvent({
event,
embedder,
guest,
browserWindowOptions,
windowOpenArgs,
disposition,
postData,
referrer
});
if (didCancelEvent) return;
const { options: parsedOptions } = parseFeatures(features);
const browserWindowOptions = {
show: true,
width: 800,
height: 600,
...parsedOptions,
...overrideBrowserWindowOptions
};
// To spec, subsequent window.open calls with the same frame name (`target` in
// spec parlance) will reuse the previous window.
@@ -134,68 +120,6 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
}
};
/**
* Deprecated in favor of `webContents.setWindowOpenHandler` and
* `did-create-window` in 11.0.0. Will be removed in 12.0.0.
*/
function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs, browserWindowOptions, disposition, referrer, postData }: {
event: { sender: WebContents, defaultPrevented: boolean, newGuest?: BrowserWindow },
embedder: WebContents,
guest?: WebContents,
windowOpenArgs: WindowOpenArgs,
browserWindowOptions: BrowserWindowConstructorOptions,
disposition: string,
referrer: Referrer,
postData?: PostData,
}): boolean {
const { url, frameName } = windowOpenArgs;
const isWebViewWithPopupsDisabled = embedder.getType() === 'webview' && embedder.getLastWebPreferences()!.disablePopups;
const postBody = postData ? {
data: postData,
...parseContentTypeFormat(postData)
} : null;
if (embedder.listenerCount('new-window') > 0) {
deprecate.log('The new-window event is deprecated and will be removed. Please use contents.setWindowOpenHandler() instead.');
}
embedder.emit(
'new-window',
event,
url,
frameName,
disposition,
{
...browserWindowOptions,
webContents: guest
},
[], // additionalFeatures
referrer,
postBody
);
const { newGuest } = event;
if (isWebViewWithPopupsDisabled) return true;
if (event.defaultPrevented) {
if (newGuest) {
if (guest === newGuest.webContents) {
// The webContents is not changed, so set defaultPrevented to false to
// stop the callers of this event from destroying the webContents.
event.defaultPrevented = false;
}
handleWindowLifecycleEvents({
embedder: event.sender,
guest: newGuest,
frameName,
outlivesOpener: false
});
}
return true;
}
return false;
}
// Security options that child windows will always inherit from parent windows
const securityWebPreferences: { [key: string]: boolean } = {
contextIsolation: true,
@@ -207,31 +131,6 @@ const securityWebPreferences: { [key: string]: boolean } = {
enableWebSQL: false
};
function makeBrowserWindowOptions ({ embedder, features, overrideOptions }: {
embedder: WebContents,
features: string,
overrideOptions?: BrowserWindowConstructorOptions,
}) {
const { options: parsedOptions, webPreferences: parsedWebPreferences } = parseFeatures(features);
return {
show: true,
width: 800,
height: 600,
...parsedOptions,
...overrideOptions,
// Note that for normal code path an existing WebContents created by
// Chromium will be used, with web preferences parsed in the
// |-will-add-new-contents| event.
// The |webPreferences| here is only used by the |new-window| event.
webPreferences: makeWebPreferences({
embedder,
insecureParsedWebPreferences: parsedWebPreferences,
secureOverrideWebPreferences: overrideOptions && overrideOptions.webPreferences
})
} as Electron.BrowserViewConstructorOptions;
}
export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
embedder: WebContents,
insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],

View File

@@ -4,6 +4,13 @@ import { IpcMainInvokeEvent } from 'electron/main';
export class IpcMainImpl extends EventEmitter {
private _invokeHandlers: Map<string, (e: IpcMainInvokeEvent, ...args: any[]) => void> = new Map();
constructor () {
super();
// Do not throw exception when channel name is "error".
this.on('error', () => {});
}
handle: Electron.IpcMain['handle'] = (method, fn) => {
if (this._invokeHandlers.has(method)) {
throw new Error(`Attempted to register a second handler for '${method}'`);

View File

@@ -1,6 +1,3 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
export const ipcMainInternal = new IpcMainImpl() as ElectronInternal.IpcMainInternal;
// Do not throw exception when channel name is "error".
ipcMainInternal.on('error', () => {});

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "21.0.0-nightly.20220803",
"version": "22.0.0-nightly.20220810",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {
@@ -27,12 +27,13 @@
"@types/stream-json": "^1.5.1",
"@types/temp": "^0.8.34",
"@types/uuid": "^3.4.6",
"@types/webpack": "^4.41.21",
"@types/webpack-env": "^1.16.3",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.17.0",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
"asar": "^3.1.0",
"aws-sdk": "^2.814.0",
"buffer": "^6.0.3",
"check-for-leaks": "^1.2.1",
"colors": "1.4.0",
"dotenv-safe": "^4.0.4",
@@ -57,6 +58,7 @@
"minimist": "^1.2.6",
"null-loader": "^4.0.0",
"pre-flight": "^1.1.0",
"process": "^0.11.10",
"remark-cli": "^10.0.0",
"remark-preset-lint-markdown-style-guide": "^4.0.0",
"semver": "^5.6.0",
@@ -69,9 +71,9 @@
"ts-loader": "^8.0.2",
"ts-node": "6.2.0",
"typescript": "^4.5.5",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"wrapper-webpack-plugin": "^2.1.0"
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"wrapper-webpack-plugin": "^2.2.0"
},
"private": true,
"scripts": {

View File

@@ -48,10 +48,10 @@ const main = async () => {
const child = cp.spawn('node', [
'./node_modules/webpack-cli/bin/cli.js',
'--config', `./build/webpack/${webpackTarget.config}`,
'--display', 'errors-only',
`--output-path=${tmpDir}`,
`--output-filename=${webpackTarget.name}.measure.js`,
'--env.PRINT_WEBPACK_GRAPH'
'--stats', 'errors-only',
'--output-path', tmpDir,
'--output-filename', `${webpackTarget.name}.measure.js`,
'--env', 'PRINT_WEBPACK_GRAPH'
], {
cwd: path.resolve(__dirname, '..')
});

View File

@@ -1151,7 +1151,9 @@ bool App::Relaunch(gin::Arguments* js_args) {
gin_helper::Dictionary options;
if (js_args->GetNext(&options)) {
if (options.Get("execPath", &exec_path) || options.Get("args", &args))
bool has_exec_path = options.Get("execPath", &exec_path);
bool has_args = options.Get("args", &args);
if (has_exec_path || has_args)
override_argv = true;
}

View File

@@ -362,6 +362,18 @@ gin::Handle<WebFrameMain> WebFrameMain::From(v8::Isolate* isolate,
return handle;
}
// static
gin::Handle<WebFrameMain> WebFrameMain::FromOrNull(
v8::Isolate* isolate,
content::RenderFrameHost* rfh) {
if (rfh == nullptr)
return gin::Handle<WebFrameMain>();
auto* web_frame = FromRenderFrameHost(rfh);
if (web_frame)
return gin::CreateHandle(isolate, web_frame);
return gin::Handle<WebFrameMain>();
}
// static
v8::Local<v8::ObjectTemplate> WebFrameMain::FillObjectTemplate(
v8::Isolate* isolate,
@@ -409,6 +421,20 @@ v8::Local<v8::Value> FromID(gin_helper::ErrorThrower thrower,
return WebFrameMain::From(thrower.isolate(), rfh).ToV8();
}
v8::Local<v8::Value> FromIDOrNull(gin_helper::ErrorThrower thrower,
int render_process_id,
int render_frame_id) {
if (!electron::Browser::Get()->is_ready()) {
thrower.ThrowError("WebFrameMain is available only after app ready");
return v8::Null(thrower.isolate());
}
auto* rfh =
content::RenderFrameHost::FromID(render_process_id, render_frame_id);
return WebFrameMain::FromOrNull(thrower.isolate(), rfh).ToV8();
}
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
@@ -417,6 +443,7 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict(isolate, exports);
dict.Set("WebFrameMain", WebFrameMain::GetConstructor(context));
dict.SetMethod("fromId", &FromID);
dict.SetMethod("fromIdOrNull", &FromIDOrNull);
}
} // namespace

View File

@@ -44,6 +44,9 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain>,
static gin::Handle<WebFrameMain> From(
v8::Isolate* isolate,
content::RenderFrameHost* render_frame_host);
static gin::Handle<WebFrameMain> FromOrNull(
v8::Isolate* isolate,
content::RenderFrameHost* render_frame_host);
static WebFrameMain* FromFrameTreeNodeId(int frame_tree_node_id);
static WebFrameMain* FromRenderFrameHost(
content::RenderFrameHost* render_frame_host);

View File

@@ -50,8 +50,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 21,0,0,20220803
PRODUCTVERSION 21,0,0,20220803
FILEVERSION 22,0,0,20220810
PRODUCTVERSION 22,0,0,20220810
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -68,12 +68,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "GitHub, Inc."
VALUE "FileDescription", "Electron"
VALUE "FileVersion", "21.0.0"
VALUE "FileVersion", "22.0.0"
VALUE "InternalName", "electron.exe"
VALUE "LegalCopyright", "Copyright (C) 2015 GitHub, Inc. All rights reserved."
VALUE "OriginalFilename", "electron.exe"
VALUE "ProductName", "Electron"
VALUE "ProductVersion", "21.0.0"
VALUE "ProductVersion", "22.0.0"
VALUE "SquirrelAwareVersion", "1"
END
END

View File

@@ -370,9 +370,11 @@ describe('app module', () => {
server!.once('error', error => done(error));
server!.on('connection', client => {
client.once('data', data => {
if (String(data) === 'false' && state === 'none') {
if (String(data) === '--first' && state === 'none') {
state = 'first-launch';
} else if (String(data) === 'true' && state === 'first-launch') {
} else if (String(data) === '--second' && state === 'first-launch') {
state = 'second-launch';
} else if (String(data) === '--third' && state === 'second-launch') {
done();
} else {
done(`Unexpected state: "${state}", data: "${data}"`);
@@ -381,7 +383,7 @@ describe('app module', () => {
});
const appPath = path.join(fixturesPath, 'api', 'relaunch');
const child = cp.spawn(process.execPath, [appPath]);
const child = cp.spawn(process.execPath, [appPath, '--first']);
child.stdout.on('data', (c) => console.log(c.toString()));
child.stderr.on('data', (c) => console.log(c.toString()));
child.on('exit', (code, signal) => {

View File

@@ -5,7 +5,7 @@ import * as fs from 'fs';
import * as qs from 'querystring';
import * as http from 'http';
import { AddressInfo } from 'net';
import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, BrowserWindowConstructorOptions } from 'electron/main';
import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents } from 'electron/main';
import { emittedOnce, emittedUntil, emittedNTimes } from './events-helpers';
import { ifit, ifdescribe, defer, delay } from './spec-helpers';
@@ -3080,7 +3080,7 @@ describe('BrowserWindow module', () => {
expect(argv).to.include('--enable-sandbox');
});
it('should open windows with the options configured via new-window event listeners', async () => {
it('should open windows with the options configured via setWindowOpenHandler handlers', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
@@ -3171,30 +3171,6 @@ describe('BrowserWindow module', () => {
});
});
it('supports calling preventDefault on new-window events', (done) => {
const w = new BrowserWindow({
show: false,
webPreferences: {
sandbox: true
}
});
const initialWebContents = webContents.getAllWebContents().map((i) => i.id);
w.webContents.once('new-window', (e) => {
e.preventDefault();
// We need to give it some time so the windows get properly disposed (at least on OSX).
setTimeout(() => {
const currentWebContents = webContents.getAllWebContents().map((i) => i.id);
try {
expect(currentWebContents).to.deep.equal(initialWebContents);
done();
} catch (error) {
done(e);
}
}, 100);
});
w.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
});
it('validates process APIs access in sandboxed renderer', async () => {
const w = new BrowserWindow({
show: false,
@@ -3350,7 +3326,7 @@ describe('BrowserWindow module', () => {
w.loadFile(path.join(fixtures, 'api', 'new-window-webview.html'));
await webviewLoaded;
});
it('should open windows with the options configured via new-window event listeners', async () => {
it('should open windows with the options configured via setWindowOpenHandler handlers', async () => {
const preloadPath = path.join(mainFixtures, 'api', 'new-window-preload.js');
w.webContents.setWindowOpenHandler(() => ({
action: 'allow',
@@ -3720,94 +3696,6 @@ describe('BrowserWindow module', () => {
});
});
describe('new-window event', () => {
afterEach(closeAllWindows);
it('emits when window.open is called', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
w.webContents.once('new-window', (e, url, frameName, disposition, options) => {
e.preventDefault();
try {
expect(url).to.equal('http://host/');
expect(frameName).to.equal('host');
expect((options as any)['this-is-not-a-standard-feature']).to.equal(true);
done();
} catch (e) {
done(e);
}
});
w.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
});
it('emits when window.open is called with no webPreferences', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.once('new-window', function (e, url, frameName, disposition, options) {
e.preventDefault();
try {
expect(url).to.equal('http://host/');
expect(frameName).to.equal('host');
expect((options as any)['this-is-not-a-standard-feature']).to.equal(true);
done();
} catch (e) {
done(e);
}
});
w.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
});
it('emits when link with target is called', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
w.webContents.once('new-window', (e, url, frameName) => {
e.preventDefault();
try {
expect(url).to.equal('http://host/');
expect(frameName).to.equal('target');
done();
} catch (e) {
done(e);
}
});
w.loadFile(path.join(fixtures, 'pages', 'target-name.html'));
});
it('includes all properties', async () => {
const w = new BrowserWindow({ show: false });
const p = new Promise<{
url: string,
frameName: string,
disposition: string,
options: BrowserWindowConstructorOptions,
additionalFeatures: string[],
referrer: Electron.Referrer,
postBody: Electron.PostBody
}>((resolve) => {
w.webContents.once('new-window', (e, url, frameName, disposition, options, additionalFeatures, referrer, postBody) => {
e.preventDefault();
resolve({ url, frameName, disposition, options, additionalFeatures, referrer, postBody });
});
});
w.loadURL(`data:text/html,${encodeURIComponent(`
<form target="_blank" method="POST" id="form" action="http://example.com/test">
<input type="text" name="post-test-key" value="post-test-value"></input>
</form>
<script>form.submit()</script>
`)}`);
const { url, frameName, disposition, options, additionalFeatures, referrer, postBody } = await p;
expect(url).to.equal('http://example.com/test');
expect(frameName).to.equal('');
expect(disposition).to.equal('foreground-tab');
expect(options).to.be.an('object').not.null();
expect(referrer.policy).to.equal('strict-origin-when-cross-origin');
expect(referrer.url).to.equal('');
expect(additionalFeatures).to.deep.equal([]);
expect(postBody.data).to.have.length(1);
expect(postBody.data[0].type).to.equal('rawData');
expect((postBody.data[0] as any).bytes).to.deep.equal(Buffer.from('post-test-key=post-test-value'));
expect(postBody.contentType).to.equal('application/x-www-form-urlencoded');
});
});
ifdescribe(process.platform !== 'linux')('max/minimize events', () => {
afterEach(closeAllWindows);
it('emits an event when window is maximized', async () => {
@@ -5255,7 +5143,8 @@ describe('BrowserWindow module', () => {
});
});
describe('contextIsolation option with and without sandbox option', () => {
// TODO (jkleinsc) renable these tests on mas arm64
ifdescribe(!process.mas || process.arch !== 'arm64')('contextIsolation option with and without sandbox option', () => {
const expectedContextData = {
preloadContext: {
preloadProperty: 'number',

View File

@@ -3,8 +3,13 @@ import { expect } from 'chai';
import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
import { closeAllWindows } from './window-helpers';
import { emittedOnce } from './events-helpers';
import { defer } from './spec-helpers';
import * as path from 'path';
import * as http from 'http';
import { AddressInfo } from 'net';
const v8Util = process._linkedBinding('electron_common_v8_util');
const fixturesPath = path.resolve(__dirname, 'fixtures');
describe('ipc module', () => {
describe('invoke', () => {
@@ -90,7 +95,7 @@ describe('ipc module', () => {
});
it('throws an error when invoking a handler that was removed', async () => {
ipcMain.handle('test', () => {});
ipcMain.handle('test', () => { });
ipcMain.removeHandler('test');
const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/No handler registered/);
@@ -101,9 +106,9 @@ describe('ipc module', () => {
});
it('forbids multiple handlers', async () => {
ipcMain.handle('test', () => {});
ipcMain.handle('test', () => { });
try {
expect(() => { ipcMain.handle('test', () => {}); }).to.throw(/second handler/);
expect(() => { ipcMain.handle('test', () => { }); }).to.throw(/second handler/);
} finally {
ipcMain.removeHandler('test');
}
@@ -563,4 +568,195 @@ describe('ipc module', () => {
generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents));
generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame));
});
describe('WebContents.ipc', () => {
afterEach(closeAllWindows);
it('receives ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
const [, num] = await emittedOnce(w.webContents.ipc, 'test');
expect(num).to.equal(42);
});
it('receives sync-ipc messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.on('test', (event, arg) => {
event.returnValue = arg * 2;
});
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])');
const [event] = await emittedOnce(w.webContents.ipc, 'test');
expect(event.ports.length).to.equal(1);
});
it('handles invoke messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('cascades to ipcMain', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
let gotFromIpcMain = false;
const ipcMainReceived = new Promise<void>(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); }));
const ipcReceived = new Promise<boolean>(resolve => w.webContents.ipc.on('test', () => { resolve(gotFromIpcMain); }));
defer(() => ipcMain.removeAllListeners('test'));
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
// assert that they are delivered in the correct order
expect(await ipcReceived).to.be.false();
await ipcMainReceived;
});
it('overrides ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => { throw new Error('should not be called'); });
defer(() => ipcMain.removeHandler('test'));
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('falls back to ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
ipcMain.handle('test', (_event, arg) => { return arg * 2; });
defer(() => ipcMain.removeHandler('test'));
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('receives ipcs from child frames', async () => {
const server = http.createServer((req, res) => {
res.setHeader('content-type', 'text/html');
res.end('');
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = (server.address() as AddressInfo).port;
defer(() => {
server.close();
});
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } });
// Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page.
await w.loadURL(`data:text/html,<iframe src="http://localhost:${port}"></iframe>`);
w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)');
const [, arg] = await emittedOnce(w.webContents.ipc, 'test');
expect(arg).to.equal(42);
});
});
describe('WebFrameMain.ipc', () => {
afterEach(closeAllWindows);
it('responds to ipc messages in the main frame', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
const [, arg] = await emittedOnce(w.webContents.mainFrame.ipc, 'test');
expect(arg).to.equal(42);
});
it('responds to sync ipc messages in the main frame', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.mainFrame.ipc.on('test', (event, arg) => {
event.returnValue = arg * 2;
});
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])');
const [event] = await emittedOnce(w.webContents.mainFrame.ipc, 'test');
expect(event.ports.length).to.equal(1);
});
it('handles invoke messages sent from the WebContents', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('cascades to WebContents and ipcMain', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
let gotFromIpcMain = false;
let gotFromWebContents = false;
const ipcMainReceived = new Promise<void>(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); }));
const ipcWebContentsReceived = new Promise<boolean>(resolve => w.webContents.ipc.on('test', () => { gotFromWebContents = true; resolve(gotFromIpcMain); }));
const ipcReceived = new Promise<boolean>(resolve => w.webContents.mainFrame.ipc.on('test', () => { resolve(gotFromWebContents); }));
defer(() => ipcMain.removeAllListeners('test'));
w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)');
// assert that they are delivered in the correct order
expect(await ipcReceived).to.be.false();
expect(await ipcWebContentsReceived).to.be.false();
await ipcMainReceived;
});
it('overrides ipcMain handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => { throw new Error('should not be called'); });
defer(() => ipcMain.removeHandler('test'));
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('overrides WebContents handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', () => { throw new Error('should not be called'); });
w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2);
ipcMain.handle('test', () => { throw new Error('should not be called'); });
defer(() => ipcMain.removeHandler('test'));
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('falls back to WebContents handlers', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
w.webContents.ipc.handle('test', (_event, arg) => { return arg * 2; });
const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)');
expect(result).to.equal(42 * 2);
});
it('receives ipcs from child frames', async () => {
const server = http.createServer((req, res) => {
res.setHeader('content-type', 'text/html');
res.end('');
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = (server.address() as AddressInfo).port;
defer(() => {
server.close();
});
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } });
// Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page.
await w.loadURL(`data:text/html,<iframe src="http://localhost:${port}"></iframe>`);
w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)');
w.webContents.mainFrame.ipc.on('test', () => { throw new Error('should not be called'); });
const [, arg] = await emittedOnce(w.webContents.mainFrame.frames[0].ipc, 'test');
expect(arg).to.equal(42);
});
});
});

View File

@@ -2102,26 +2102,4 @@ describe('webContents module', () => {
expect(params.y).to.be.a('number');
});
});
it('emits a cancelable event before creating a child webcontents', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
sandbox: true
}
});
w.webContents.on('-will-add-new-contents' as any, (event: any, url: any) => {
expect(url).to.equal('about:blank');
event.preventDefault();
});
let wasCalled = false;
w.webContents.on('new-window' as any, () => {
wasCalled = true;
});
await w.loadURL('about:blank');
await w.webContents.executeJavaScript('window.open(\'about:blank\')');
await new Promise((resolve) => { process.nextTick(resolve); });
expect(wasCalled).to.equal(false);
await closeAllWindows();
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ import * as url from 'url';
import * as ChildProcess from 'child_process';
import { EventEmitter } from 'events';
import { promisify } from 'util';
import { ifit, ifdescribe, defer, delay } from './spec-helpers';
import { ifit, ifdescribe, defer, delay, itremote } from './spec-helpers';
import { AddressInfo } from 'net';
import { PipeTransport } from './pipe-transport';
import * as ws from 'ws';
@@ -894,6 +894,27 @@ describe('chromium features', () => {
});
}
// FIXME(zcbenz): This test is making the spec runner hang on exit on Windows.
ifit(process.platform !== 'win32')('disables node integration when it is disabled on the parent window', async () => {
const windowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'window-opener-no-node-integration.html'));
windowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-node.html`);
const w = new BrowserWindow({ show: false });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const { eventData } = await w.webContents.executeJavaScript(`(async () => {
const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=false')
const e = await message
b.close();
return {
eventData: e.data
}
})()`);
expect(eventData.isProcessGlobalUndefined).to.be.true();
});
it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => {
// NB. webSecurity is disabled because native window.open() is not
// allowed to load devtools:// URLs.
@@ -1007,6 +1028,79 @@ describe('chromium features', () => {
});
expect(frameName).to.equal('__proto__');
});
// TODO(nornagon): I'm not sure this ... ever was correct?
it.skip('inherit options of parent window', async () => {
const w = new BrowserWindow({ show: false, width: 123, height: 456 });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const url = `file://${fixturesPath}/pages/window-open-size.html`;
const { width, height, eventData } = await w.webContents.executeJavaScript(`(async () => {
const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
const b = window.open(${JSON.stringify(url)}, '', 'show=false')
const e = await message
b.close();
const width = outerWidth;
const height = outerHeight;
return {
width,
height,
eventData: e.data
}
})()`);
expect(eventData).to.equal(`size: ${width} ${height}`);
expect(eventData).to.equal('size: 123 456');
});
it('does not override child options', async () => {
const w = new BrowserWindow({ show: false });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const windowUrl = `file://${fixturesPath}/pages/window-open-size.html`;
const { eventData } = await w.webContents.executeJavaScript(`(async () => {
const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no,width=350,height=450')
const e = await message
b.close();
return { eventData: e.data }
})()`);
expect(eventData).to.equal('size: 350 450');
});
it('disables the <webview> tag when it is disabled on the parent window', async () => {
const windowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'window-opener-no-webview-tag.html'));
windowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-webview.html`);
const w = new BrowserWindow({ show: false });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const { eventData } = await w.webContents.executeJavaScript(`(async () => {
const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
const b = window.open(${JSON.stringify(windowUrl)}, '', 'webviewTag=no,contextIsolation=no,nodeIntegration=yes,show=no')
const e = await message
b.close();
return { eventData: e.data }
})()`);
expect(eventData.isWebViewGlobalUndefined).to.be.true();
});
it('throws an exception when the arguments cannot be converted to strings', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL('about:blank');
await expect(
w.webContents.executeJavaScript('window.open(\'\', { toString: null })')
).to.eventually.be.rejected();
await expect(
w.webContents.executeJavaScript('window.open(\'\', \'\', { toString: 3 })')
).to.eventually.be.rejected();
});
it('does not throw an exception when the features include webPreferences', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL('about:blank');
await expect(
w.webContents.executeJavaScript('window.open(\'\', \'\', \'show=no,webPreferences=\'); null')
).to.eventually.be.fulfilled();
});
});
describe('window.opener', () => {
@@ -1023,6 +1117,92 @@ describe('chromium features', () => {
expect(channel).to.equal('opener');
expect(opener).to.equal(null);
});
it('is not null for window opened by window.open', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const windowUrl = `file://${fixturesPath}/pages/window-opener.html`;
const eventData = await w.webContents.executeJavaScript(`
const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no');
new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => e.data);
`);
expect(eventData).to.equal('object');
});
});
describe('window.opener.postMessage', () => {
it('sets source and origin correctly', async () => {
const w = new BrowserWindow({ show: false });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const windowUrl = `file://${fixturesPath}/pages/window-opener-postMessage.html`;
const { sourceIsChild, origin } = await w.webContents.executeJavaScript(`
const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no');
new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => ({
sourceIsChild: e.source === b,
origin: e.origin
}));
`);
expect(sourceIsChild).to.be.true();
expect(origin).to.equal('file://');
});
it('supports windows opened from a <webview>', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
w.loadURL('about:blank');
const childWindowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'webview-opener-postMessage.html'));
childWindowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-postMessage.html`);
const message = await w.webContents.executeJavaScript(`
const webview = new WebView();
webview.allowpopups = true;
webview.setAttribute('webpreferences', 'contextIsolation=no');
webview.src = ${JSON.stringify(childWindowUrl)}
const consoleMessage = new Promise(resolve => webview.addEventListener('console-message', resolve, {once: true}));
document.body.appendChild(webview);
consoleMessage.then(e => e.message)
`);
expect(message).to.equal('message');
});
describe('targetOrigin argument', () => {
let serverURL: string;
let server: any;
beforeEach((done) => {
server = http.createServer((req, res) => {
res.writeHead(200);
const filePath = path.join(fixturesPath, 'pages', 'window-opener-targetOrigin.html');
res.end(fs.readFileSync(filePath, 'utf8'));
});
server.listen(0, '127.0.0.1', () => {
serverURL = `http://127.0.0.1:${server.address().port}`;
done();
});
});
afterEach(() => {
server.close();
});
it('delivers messages that match the origin', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const data = await w.webContents.executeJavaScript(`
window.open(${JSON.stringify(serverURL)}, '', 'show=no,contextIsolation=no,nodeIntegration=yes');
new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => e.data)
`);
expect(data).to.equal('deliver');
});
});
});
describe('navigator.mediaDevices', () => {
@@ -1523,6 +1703,67 @@ describe('chromium features', () => {
expect(childError).to.be.null();
});
});
describe('DOM storage quota increase', () => {
['localStorage', 'sessionStorage'].forEach((storageName) => {
it(`allows saving at least 40MiB in ${storageName}`, async () => {
const w = new BrowserWindow({ show: false });
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
// Although JavaScript strings use UTF-16, the underlying
// storage provider may encode strings differently, muddling the
// translation between character and byte counts. However,
// a string of 40 * 2^20 characters will require at least 40MiB
// and presumably no more than 80MiB, a size guaranteed to
// to exceed the original 10MiB quota yet stay within the
// new 100MiB quota.
// Note that both the key name and value affect the total size.
const testKeyName = '_electronDOMStorageQuotaIncreasedTest';
const length = 40 * Math.pow(2, 20) - testKeyName.length;
await w.webContents.executeJavaScript(`
${storageName}.setItem(${JSON.stringify(testKeyName)}, 'X'.repeat(${length}));
`);
// Wait at least one turn of the event loop to help avoid false positives
// Although not entirely necessary, the previous version of this test case
// failed to detect a real problem (perhaps related to DOM storage data caching)
// wherein calling `getItem` immediately after `setItem` would appear to work
// but then later (e.g. next tick) it would not.
await delay(1);
try {
const storedLength = await w.webContents.executeJavaScript(`${storageName}.getItem(${JSON.stringify(testKeyName)}).length`);
expect(storedLength).to.equal(length);
} finally {
await w.webContents.executeJavaScript(`${storageName}.removeItem(${JSON.stringify(testKeyName)});`);
}
});
it(`throws when attempting to use more than 128MiB in ${storageName}`, async () => {
const w = new BrowserWindow({ show: false });
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
await expect((async () => {
const testKeyName = '_electronDOMStorageQuotaStillEnforcedTest';
const length = 128 * Math.pow(2, 20) - testKeyName.length;
try {
await w.webContents.executeJavaScript(`
${storageName}.setItem(${JSON.stringify(testKeyName)}, 'X'.repeat(${length}));
`);
} finally {
await w.webContents.executeJavaScript(`${storageName}.removeItem(${JSON.stringify(testKeyName)});`);
}
})()).to.eventually.be.rejected();
});
});
});
describe('persistent storage', () => {
it('can be requested', async () => {
const w = new BrowserWindow({ show: false });
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
const grantedBytes = await w.webContents.executeJavaScript(`new Promise(resolve => {
navigator.webkitPersistentStorage.requestQuota(1024 * 1024, resolve);
})`);
expect(grantedBytes).to.equal(1048576);
});
});
});
ifdescribe(features.isPDFViewerEnabled())('PDF Viewer', () => {
@@ -1791,6 +2032,125 @@ describe('chromium features', () => {
expect(x).to.deep.equal(new Uint8Array([116, 101, 115, 116]));
});
});
describe('Promise', () => {
before(() => {
ipcMain.handle('ping', (e, arg) => arg);
});
after(() => {
ipcMain.removeHandler('ping');
});
itremote('resolves correctly in Node.js calls', async () => {
await new Promise<void>((resolve, reject) => {
class XElement extends HTMLElement {}
customElements.define('x-element', XElement);
setImmediate(() => {
let called = false;
Promise.resolve().then(() => {
if (called) resolve();
else reject(new Error('wrong sequence'));
});
document.createElement('x-element');
called = true;
});
});
});
itremote('resolves correctly in Electron calls', async () => {
await new Promise<void>((resolve, reject) => {
class YElement extends HTMLElement {}
customElements.define('y-element', YElement);
require('electron').ipcRenderer.invoke('ping').then(() => {
let called = false;
Promise.resolve().then(() => {
if (called) resolve();
else reject(new Error('wrong sequence'));
});
document.createElement('y-element');
called = true;
});
});
});
});
describe('synchronous prompts', () => {
describe('window.alert(message, title)', () => {
itremote('throws an exception when the arguments cannot be converted to strings', () => {
expect(() => {
window.alert({ toString: null });
}).to.throw('Cannot convert object to primitive value');
});
});
describe('window.confirm(message, title)', () => {
itremote('throws an exception when the arguments cannot be converted to strings', () => {
expect(() => {
(window.confirm as any)({ toString: null }, 'title');
}).to.throw('Cannot convert object to primitive value');
});
});
});
describe('window.history', () => {
describe('window.history.go(offset)', () => {
itremote('throws an exception when the argument cannot be converted to a string', () => {
expect(() => {
(window.history.go as any)({ toString: null });
}).to.throw('Cannot convert object to primitive value');
});
});
});
describe('console functions', () => {
itremote('should exist', () => {
expect(console.log, 'log').to.be.a('function');
expect(console.error, 'error').to.be.a('function');
expect(console.warn, 'warn').to.be.a('function');
expect(console.info, 'info').to.be.a('function');
expect(console.debug, 'debug').to.be.a('function');
expect(console.trace, 'trace').to.be.a('function');
expect(console.time, 'time').to.be.a('function');
expect(console.timeEnd, 'timeEnd').to.be.a('function');
});
});
ifdescribe(features.isTtsEnabled())('SpeechSynthesis', () => {
before(function () {
// TODO(nornagon): this is broken on CI, it triggers:
// [FATAL:speech_synthesis.mojom-shared.h(237)] The outgoing message will
// trigger VALIDATION_ERROR_UNEXPECTED_NULL_POINTER at the receiving side
// (null text in SpeechSynthesisUtterance struct).
this.skip();
});
itremote('should emit lifecycle events', async () => {
const sentence = `long sentence which will take at least a few seconds to
utter so that it's possible to pause and resume before the end`;
const utter = new SpeechSynthesisUtterance(sentence);
// Create a dummy utterance so that speech synthesis state
// is initialized for later calls.
speechSynthesis.speak(new SpeechSynthesisUtterance());
speechSynthesis.cancel();
speechSynthesis.speak(utter);
// paused state after speak()
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onstart = resolve; });
// paused state after start event
expect(speechSynthesis.paused).to.be.false();
speechSynthesis.pause();
// paused state changes async, right before the pause event
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onpause = resolve; });
expect(speechSynthesis.paused).to.be.true();
speechSynthesis.resume();
await new Promise((resolve) => { utter.onresume = resolve; });
// paused state after resume event
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onend = resolve; });
});
});
});
describe('font fallback', () => {

View File

@@ -0,0 +1,14 @@
const { contextBridge, ipcRenderer } = require('electron');
// NOTE: Never do this in an actual app! Very insecure!
contextBridge.exposeInMainWorld('ipc', {
send (...args) {
return ipcRenderer.send(...args);
},
sendSync (...args) {
return ipcRenderer.sendSync(...args);
},
invoke (...args) {
return ipcRenderer.invoke(...args);
}
});

View File

@@ -1,161 +0,0 @@
[
[
"top=5,left=10,resizable=no",
{
"sender": "[WebContents]"
},
"about:blank",
"frame-name",
"new-window",
{
"show": true,
"width": 800,
"height": 600,
"top": 5,
"left": 10,
"resizable": false,
"x": 10,
"y": 5,
"webPreferences": {
"contextIsolation": true,
"nodeIntegration": false,
"sandbox": true,
"webviewTag": false,
"nodeIntegrationInSubFrames": false
},
"webContents": "[WebContents]"
},
[],
{
"url": "",
"policy": "strict-origin-when-cross-origin"
},
null
],
[
"zoomFactor=2,resizable=0,x=0,y=10",
{
"sender": "[WebContents]"
},
"about:blank",
"frame-name",
"new-window",
{
"show": true,
"width": 800,
"height": 600,
"resizable": false,
"x": 0,
"y": 10,
"webPreferences": {
"zoomFactor": "2",
"contextIsolation": true,
"nodeIntegration": false,
"sandbox": true,
"webviewTag": false,
"nodeIntegrationInSubFrames": false
},
"webContents": "[WebContents]"
},
[],
{
"url": "",
"policy": "strict-origin-when-cross-origin"
},
null
],
[
"backgroundColor=gray,webPreferences=0,x=100,y=100",
{
"sender": "[WebContents]"
},
"about:blank",
"frame-name",
"new-window",
{
"show": true,
"width": 800,
"height": 600,
"backgroundColor": "gray",
"webPreferences": {
"contextIsolation": true,
"nodeIntegration": false,
"sandbox": true,
"webviewTag": false,
"nodeIntegrationInSubFrames": false
},
"x": 100,
"y": 100,
"webContents": "[WebContents]"
},
[],
{
"url": "",
"policy": "strict-origin-when-cross-origin"
},
null
],
[
"x=50,y=20,title=sup",
{
"sender": "[WebContents]"
},
"about:blank",
"frame-name",
"new-window",
{
"show": true,
"width": 800,
"height": 600,
"x": 50,
"y": 20,
"title": "sup",
"webPreferences": {
"contextIsolation": true,
"nodeIntegration": false,
"sandbox": true,
"webviewTag": false,
"nodeIntegrationInSubFrames": false
},
"webContents": "[WebContents]"
},
[],
{
"url": "",
"policy": "strict-origin-when-cross-origin"
},
null
],
[
"show=false,top=1,left=1",
{
"sender": "[WebContents]"
},
"about:blank",
"frame-name",
"new-window",
{
"show": false,
"width": 800,
"height": 600,
"top": 1,
"left": 1,
"x": 1,
"y": 1,
"webPreferences": {
"contextIsolation": true,
"nodeIntegration": false,
"sandbox": true,
"webviewTag": false,
"nodeIntegrationInSubFrames": false
},
"webContents": "[WebContents]"
},
[],
{
"url": "",
"policy": "strict-origin-when-cross-origin"
},
null
]
]

View File

@@ -1,75 +1,8 @@
import { BrowserWindow } from 'electron';
import { writeFileSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { expect, assert } from 'chai';
import { closeAllWindows } from './window-helpers';
const { emittedOnce } = require('./events-helpers');
function genSnapshot (browserWindow: BrowserWindow, features: string) {
return new Promise((resolve) => {
browserWindow.webContents.on('new-window', (...args: any[]) => {
resolve([features, ...args]);
});
browserWindow.webContents.executeJavaScript(`window.open('about:blank', 'frame-name', '${features}') && true`);
});
}
describe('new-window event', () => {
const snapshotFileName = 'native-window-open.snapshot.txt';
const browserWindowOptions = {
show: false,
width: 200,
title: 'cool',
backgroundColor: 'blue',
focusable: false,
webPreferences: {
sandbox: true
}
};
const snapshotFile = resolve(__dirname, 'fixtures', 'snapshots', snapshotFileName);
let browserWindow: BrowserWindow;
let existingSnapshots: any[];
before(() => {
existingSnapshots = parseSnapshots(readFileSync(snapshotFile, { encoding: 'utf8' }));
});
beforeEach((done) => {
browserWindow = new BrowserWindow(browserWindowOptions);
browserWindow.loadURL('about:blank');
browserWindow.on('ready-to-show', () => { done(); });
});
afterEach(closeAllWindows);
const newSnapshots: any[] = [];
[
'top=5,left=10,resizable=no',
'zoomFactor=2,resizable=0,x=0,y=10',
'backgroundColor=gray,webPreferences=0,x=100,y=100',
'x=50,y=20,title=sup',
'show=false,top=1,left=1'
].forEach((features, index) => {
/**
* ATTN: If this test is failing, you likely just need to change
* `shouldOverwriteSnapshot` to true and then evaluate the snapshot diff
* to see if the change is harmless.
*/
it(`matches snapshot for ${features}`, async () => {
const newSnapshot = await genSnapshot(browserWindow, features);
newSnapshots.push(newSnapshot);
// TODO: The output when these fail could be friendlier.
expect(stringifySnapshots(newSnapshot)).to.equal(stringifySnapshots(existingSnapshots[index]));
});
});
after(() => {
const shouldOverwriteSnapshot = false;
if (shouldOverwriteSnapshot) writeFileSync(snapshotFile, stringifySnapshots(newSnapshots, true));
});
});
describe('webContents.setWindowOpenHandler', () => {
let browserWindow: BrowserWindow;
beforeEach(async () => {
@@ -95,10 +28,6 @@ describe('webContents.setWindowOpenHandler', () => {
}
});
browserWindow.webContents.on('new-window', () => {
assert.fail('new-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
});
@@ -118,10 +47,6 @@ describe('webContents.setWindowOpenHandler', () => {
});
});
browserWindow.webContents.on('new-window', () => {
assert.fail('new-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
});
@@ -138,9 +63,6 @@ describe('webContents.setWindowOpenHandler', () => {
return { action: 'deny' };
});
});
browserWindow.webContents.on('new-window', () => {
assert.fail('new-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
@@ -158,9 +80,6 @@ describe('webContents.setWindowOpenHandler', () => {
return { action: 'deny' };
});
});
browserWindow.webContents.on('new-window', () => {
assert.fail('new-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
@@ -180,9 +99,6 @@ describe('webContents.setWindowOpenHandler', () => {
return { action: 'deny' };
});
});
browserWindow.webContents.on('new-window', () => {
assert.fail('new-window should not be called with an overridden window.open');
});
browserWindow.webContents.on('did-create-window', () => {
assert.fail('did-create-window should not be called with an overridden window.open');
@@ -257,10 +173,7 @@ describe('webContents.setWindowOpenHandler', () => {
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
});
await Promise.all([
emittedOnce(browserWindow.webContents, 'did-create-window'),
emittedOnce(browserWindow.webContents, 'new-window')
]);
await emittedOnce(browserWindow.webContents, 'did-create-window');
});
it('can change webPreferences of child windows', (done) => {
@@ -282,22 +195,3 @@ describe('webContents.setWindowOpenHandler', () => {
expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
});
});
function stringifySnapshots (snapshots: any, pretty = false) {
return JSON.stringify(snapshots, (key, value) => {
if (['sender', 'webContents'].includes(key)) {
return '[WebContents]';
}
if (key === 'processId' && typeof value === 'number') {
return 'placeholder-process-id';
}
if (key === 'returnValue') {
return 'placeholder-guest-contents-id';
}
return value;
}, pretty ? 2 : undefined);
}
function parseSnapshots (snapshotsJson: string) {
return JSON.parse(snapshotsJson);
}

View File

@@ -4,14 +4,16 @@ import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { emittedOnce } from './events-helpers';
import { ifdescribe, ifit } from './spec-helpers';
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './spec-helpers';
import { webContents, WebContents } from 'electron/main';
import { EventEmitter } from 'stream';
const features = process._linkedBinding('electron_common_features');
const mainFixturesPath = path.resolve(__dirname, 'fixtures');
describe('node feature', () => {
const fixtures = path.join(__dirname, '..', 'spec', 'fixtures');
describe('child_process', () => {
describe('child_process.fork', () => {
it('Works in browser process', async () => {
@@ -24,6 +26,132 @@ describe('node feature', () => {
});
});
ifdescribe(features.isRunAsNodeEnabled())('child_process in renderer', () => {
useRemoteContext();
describe('child_process.fork', () => {
itremote('works in current process', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'ping.js'));
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(msg).to.equal('message');
}, [fixtures]);
itremote('preserves args', async (fixtures: string) => {
const args = ['--expose_gc', '-test', '1'];
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'process_args.js'), args);
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(args).to.deep.equal(msg.slice(2));
}, [fixtures]);
itremote('works in forked process', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'fork_ping.js'));
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(msg).to.equal('message');
}, [fixtures]);
itremote('works in forked process when options.env is specified', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'fork_ping.js'), [], {
path: process.env.PATH
});
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(msg).to.equal('message');
}, [fixtures]);
itremote('has String::localeCompare working in script', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'locale-compare.js'));
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(msg).to.deep.equal([0, -1, 1]);
}, [fixtures]);
itremote('has setImmediate working in script', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'set-immediate.js'));
const message = new Promise<any>(resolve => child.once('message', resolve));
child.send('message');
const msg = await message;
expect(msg).to.equal('ok');
}, [fixtures]);
itremote('pipes stdio', async (fixtures: string) => {
const child = require('child_process').fork(require('path').join(fixtures, 'module', 'process-stdout.js'), { silent: true });
let data = '';
child.stdout.on('data', (chunk: any) => {
data += String(chunk);
});
const code = await new Promise<any>(resolve => child.once('close', resolve));
expect(code).to.equal(0);
expect(data).to.equal('pipes stdio');
}, [fixtures]);
itremote('works when sending a message to a process forked with the --eval argument', async () => {
const source = "process.on('message', (message) => { process.send(message) })";
const forked = require('child_process').fork('--eval', [source]);
const message = new Promise(resolve => forked.once('message', resolve));
forked.send('hello');
const msg = await message;
expect(msg).to.equal('hello');
});
it('has the electron version in process.versions', async () => {
const source = 'process.send(process.versions)';
const forked = require('child_process').fork('--eval', [source]);
const message = await new Promise(resolve => forked.once('message', resolve));
expect(message)
.to.have.own.property('electron')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
});
});
describe('child_process.spawn', () => {
itremote('supports spawning Electron as a node process via the ELECTRON_RUN_AS_NODE env var', async (fixtures: string) => {
const child = require('child_process').spawn(process.execPath, [require('path').join(fixtures, 'module', 'run-as-node.js')], {
env: {
ELECTRON_RUN_AS_NODE: true
}
});
let output = '';
child.stdout.on('data', (data: any) => {
output += data;
});
try {
await new Promise(resolve => child.stdout.once('close', resolve));
expect(JSON.parse(output)).to.deep.equal({
stdoutType: 'pipe',
processType: 'undefined',
window: 'undefined'
});
} finally {
child.kill();
}
}, [fixtures]);
});
describe('child_process.exec', () => {
ifit(process.platform === 'linux')('allows executing a setuid binary from non-sandboxed renderer', async () => {
// Chrome uses prctl(2) to set the NO_NEW_PRIVILEGES flag on Linux (see
// https://github.com/torvalds/linux/blob/40fde647cc/Documentation/userspace-api/no_new_privs.rst).
// We disable this for unsandboxed processes, which the renderer tests
// are running in. If this test fails with an error like 'effective uid
// is not 0', then it's likely that our patch to prevent the flag from
// being set has become ineffective.
const w = await getRemoteContext();
const stdout = await w.webContents.executeJavaScript('require(\'child_process\').execSync(\'sudo --help\')');
expect(stdout).to.not.be.empty();
});
});
});
it('does not hang when using the fs module in the renderer process', async () => {
const appPath = path.join(mainFixturesPath, 'apps', 'libuv-hang', 'main.js');
const appProcess = childProcess.spawn(process.execPath, [appPath], {
@@ -62,6 +190,344 @@ describe('node feature', () => {
interval = setInterval(clear, 10);
});
});
const suspendListeners = (emitter: EventEmitter, eventName: string, callback: (...args: any[]) => void) => {
const listeners = emitter.listeners(eventName) as ((...args: any[]) => void)[];
emitter.removeAllListeners(eventName);
emitter.once(eventName, (...args) => {
emitter.removeAllListeners(eventName);
listeners.forEach((listener) => {
emitter.on(eventName, listener);
});
// eslint-disable-next-line standard/no-callback-literal
callback(...args);
});
};
describe('error thrown in main process node context', () => {
it('gets emitted as a process uncaughtException event', async () => {
fs.readFile(__filename, () => {
throw new Error('hello');
});
const result = await new Promise(resolve => suspendListeners(process, 'uncaughtException', (error) => {
resolve(error.message);
}));
expect(result).to.equal('hello');
});
});
describe('promise rejection in main process node context', () => {
it('gets emitted as a process unhandledRejection event', async () => {
fs.readFile(__filename, () => {
Promise.reject(new Error('hello'));
});
const result = await new Promise(resolve => suspendListeners(process, 'unhandledRejection', (error) => {
resolve(error.message);
}));
expect(result).to.equal('hello');
});
});
});
describe('contexts in renderer', () => {
useRemoteContext();
describe('setTimeout in fs callback', () => {
itremote('does not crash', async (filename: string) => {
await new Promise(resolve => require('fs').readFile(filename, () => {
setTimeout(resolve, 0);
}));
}, [__filename]);
});
describe('error thrown in renderer process node context', () => {
itremote('gets emitted as a process uncaughtException event', async (filename: string) => {
const error = new Error('boo!');
require('fs').readFile(filename, () => {
throw error;
});
await new Promise<void>((resolve, reject) => {
process.once('uncaughtException', (thrown) => {
try {
expect(thrown).to.equal(error);
resolve();
} catch (e) {
reject(e);
}
});
});
}, [__filename]);
});
describe('URL handling in the renderer process', () => {
itremote('can successfully handle WHATWG URLs constructed by Blink', (fixtures: string) => {
const url = new URL('file://' + require('path').resolve(fixtures, 'pages', 'base-page.html'));
expect(() => {
require('fs').createReadStream(url);
}).to.not.throw();
}, [fixtures]);
});
describe('setTimeout called under blink env in renderer process', () => {
itremote('can be scheduled in time', async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
itremote('works from the timers module', async () => {
await new Promise(resolve => require('timers').setTimeout(resolve, 10));
});
});
describe('setInterval called under blink env in renderer process', () => {
itremote('can be scheduled in time', async () => {
await new Promise<void>(resolve => {
const id = setInterval(() => {
clearInterval(id);
resolve();
}, 10);
});
});
itremote('can be scheduled in time from timers module', async () => {
const { setInterval, clearInterval } = require('timers');
await new Promise<void>(resolve => {
const id = setInterval(() => {
clearInterval(id);
resolve();
}, 10);
});
});
});
});
describe('message loop in renderer', () => {
useRemoteContext();
describe('process.nextTick', () => {
itremote('emits the callback', () => new Promise(resolve => process.nextTick(resolve)));
itremote('works in nested calls', () =>
new Promise(resolve => {
process.nextTick(() => {
process.nextTick(() => process.nextTick(resolve));
});
}));
});
describe('setImmediate', () => {
itremote('emits the callback', () => new Promise(resolve => setImmediate(resolve)));
itremote('works in nested calls', () => new Promise(resolve => {
setImmediate(() => {
setImmediate(() => setImmediate(resolve));
});
}));
});
});
ifdescribe(features.isRunAsNodeEnabled() && process.platform === 'darwin')('net.connect', () => {
itremote('emit error when connect to a socket path without listeners', async (fixtures: string) => {
const socketPath = require('path').join(require('os').tmpdir(), 'electron-test.sock');
const script = require('path').join(fixtures, 'module', 'create_socket.js');
const child = require('child_process').fork(script, [socketPath]);
const code = await new Promise(resolve => child.once('exit', resolve));
expect(code).to.equal(0);
const client = require('net').connect(socketPath);
const error = await new Promise<any>(resolve => client.once('error', resolve));
expect(error.code).to.equal('ECONNREFUSED');
}, [fixtures]);
});
describe('Buffer', () => {
useRemoteContext();
itremote('can be created from WebKit external string', () => {
const p = document.createElement('p');
p.innerText = '闲云潭影日悠悠,物换星移几度秋';
const b = Buffer.from(p.innerText);
expect(b.toString()).to.equal('闲云潭影日悠悠,物换星移几度秋');
expect(Buffer.byteLength(p.innerText)).to.equal(45);
});
itremote('correctly parses external one-byte UTF8 string', () => {
const p = document.createElement('p');
p.innerText = 'Jøhänñéß';
const b = Buffer.from(p.innerText);
expect(b.toString()).to.equal('Jøhänñéß');
expect(Buffer.byteLength(p.innerText)).to.equal(13);
});
itremote('does not crash when creating large Buffers', () => {
let buffer = Buffer.from(new Array(4096).join(' '));
expect(buffer.length).to.equal(4095);
buffer = Buffer.from(new Array(4097).join(' '));
expect(buffer.length).to.equal(4096);
});
itremote('does not crash for crypto operations', () => {
const crypto = require('crypto');
const data = 'lG9E+/g4JmRmedDAnihtBD4Dfaha/GFOjd+xUOQI05UtfVX3DjUXvrS98p7kZQwY3LNhdiFo7MY5rGft8yBuDhKuNNag9vRx/44IuClDhdQ=';
const key = 'q90K9yBqhWZnAMCMTOJfPQ==';
const cipherText = '{"error_code":114,"error_message":"Tham số không hợp lệ","data":null}';
for (let i = 0; i < 10000; ++i) {
const iv = Buffer.from('0'.repeat(32), 'hex');
const input = Buffer.from(data, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), iv);
const result = Buffer.concat([decipher.update(input), decipher.final()]).toString('utf8');
expect(cipherText).to.equal(result);
}
});
itremote('does not crash when using crypto.diffieHellman() constructors', () => {
const crypto = require('crypto');
crypto.createDiffieHellman('abc');
crypto.createDiffieHellman('abc', 2);
// Needed to test specific DiffieHellman ctors.
// eslint-disable-next-line no-octal
crypto.createDiffieHellman('abc', Buffer.from([2]));
// eslint-disable-next-line no-octal
crypto.createDiffieHellman('abc', '123');
});
itremote('does not crash when calling crypto.createPrivateKey() with an unsupported algorithm', () => {
const crypto = require('crypto');
const ed448 = {
crv: 'Ed448',
x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA',
d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n',
kty: 'OKP'
};
expect(() => {
crypto.createPrivateKey({ key: ed448, format: 'jwk' });
}).to.throw(/Invalid JWK data/);
});
});
describe('process.stdout', () => {
useRemoteContext();
itremote('does not throw an exception when accessed', () => {
expect(() => process.stdout).to.not.throw();
});
itremote('does not throw an exception when calling write()', () => {
expect(() => {
process.stdout.write('test');
}).to.not.throw();
});
// TODO: figure out why process.stdout.isTTY is true on Darwin but not Linux/Win.
ifdescribe(process.platform !== 'darwin')('isTTY', () => {
itremote('should be undefined in the renderer process', function () {
expect(process.stdout.isTTY).to.be.undefined();
});
});
});
describe('process.stdin', () => {
useRemoteContext();
itremote('does not throw an exception when accessed', () => {
expect(() => process.stdin).to.not.throw();
});
itremote('returns null when read from', () => {
expect(process.stdin.read()).to.be.null();
});
});
describe('process.version', () => {
itremote('should not have -pre', () => {
expect(process.version.endsWith('-pre')).to.be.false();
});
});
describe('vm.runInNewContext', () => {
itremote('should not crash', () => {
require('vm').runInNewContext('');
});
});
describe('crypto', () => {
useRemoteContext();
itremote('should list the ripemd160 hash in getHashes', () => {
expect(require('crypto').getHashes()).to.include('ripemd160');
});
itremote('should be able to create a ripemd160 hash and use it', () => {
const hash = require('crypto').createHash('ripemd160');
hash.update('electron-ripemd160');
expect(hash.digest('hex')).to.equal('fa7fec13c624009ab126ebb99eda6525583395fe');
});
itremote('should list aes-{128,256}-cfb in getCiphers', () => {
expect(require('crypto').getCiphers()).to.include.members(['aes-128-cfb', 'aes-256-cfb']);
});
itremote('should be able to create an aes-128-cfb cipher', () => {
require('crypto').createCipheriv('aes-128-cfb', '0123456789abcdef', '0123456789abcdef');
});
itremote('should be able to create an aes-256-cfb cipher', () => {
require('crypto').createCipheriv('aes-256-cfb', '0123456789abcdef0123456789abcdef', '0123456789abcdef');
});
itremote('should be able to create a bf-{cbc,cfb,ecb} ciphers', () => {
require('crypto').createCipheriv('bf-cbc', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
require('crypto').createCipheriv('bf-cfb', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
require('crypto').createCipheriv('bf-ecb', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
});
itremote('should list des-ede-cbc in getCiphers', () => {
expect(require('crypto').getCiphers()).to.include('des-ede-cbc');
});
itremote('should be able to create an des-ede-cbc cipher', () => {
const key = Buffer.from('0123456789abcdeff1e0d3c2b5a49786', 'hex');
const iv = Buffer.from('fedcba9876543210', 'hex');
require('crypto').createCipheriv('des-ede-cbc', key, iv);
});
itremote('should not crash when getting an ECDH key', () => {
const ecdh = require('crypto').createECDH('prime256v1');
expect(ecdh.generateKeys()).to.be.an.instanceof(Buffer);
expect(ecdh.getPrivateKey()).to.be.an.instanceof(Buffer);
});
itremote('should not crash when generating DH keys or fetching DH fields', () => {
const dh = require('crypto').createDiffieHellman('modp15');
expect(dh.generateKeys()).to.be.an.instanceof(Buffer);
expect(dh.getPublicKey()).to.be.an.instanceof(Buffer);
expect(dh.getPrivateKey()).to.be.an.instanceof(Buffer);
expect(dh.getPrime()).to.be.an.instanceof(Buffer);
expect(dh.getGenerator()).to.be.an.instanceof(Buffer);
});
itremote('should not crash when creating an ECDH cipher', () => {
const crypto = require('crypto');
const dh = crypto.createECDH('prime256v1');
dh.generateKeys();
dh.setPrivateKey(dh.getPrivateKey());
});
});
itremote('includes the electron version in process.versions', () => {
expect(process.versions)
.to.have.own.property('electron')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
});
itremote('includes the chrome version in process.versions', () => {
expect(process.versions)
.to.have.own.property('chrome')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+\.\d+$/);
});
describe('NODE_OPTIONS', () => {
@@ -113,8 +579,9 @@ describe('node feature', () => {
child.kill();
});
const appPath = path.join(fixtures, 'module', 'noop.js');
const env = { ...process.env, NODE_OPTIONS: '--use-openssl-ca' };
child = childProcess.spawn(process.execPath, ['--enable-logging'], { env });
child = childProcess.spawn(process.execPath, ['--enable-logging', appPath], { env });
let output = '';
const cleanup = () => {

View File

@@ -3,6 +3,8 @@ import * as path from 'path';
import * as http from 'http';
import * as v8 from 'v8';
import { SuiteFunction, TestFunction } from 'mocha';
import { BrowserWindow } from 'electron/main';
import { AssertionError } from 'chai';
const addOnly = <T>(fn: Function): T => {
const wrapped = (...args: any[]) => {
@@ -145,3 +147,47 @@ export async function repeatedly<T> (
if (+new Date() - begin > timeLimit) { throw new Error(`repeatedly timed out (limit=${timeLimit})`); }
}
}
async function makeRemoteContext (opts?: any) {
const { webPreferences, setup, url = 'about:blank', ...rest } = opts ?? {};
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, ...webPreferences }, ...rest });
await w.loadURL(url.toString());
if (setup) await w.webContents.executeJavaScript(setup);
return w;
}
const remoteContext: BrowserWindow[] = [];
export async function getRemoteContext () {
if (remoteContext.length) { return remoteContext[0]; }
const w = await makeRemoteContext();
defer(() => w.close());
return w;
}
export function useRemoteContext (opts?: any) {
before(async () => {
remoteContext.unshift(await makeRemoteContext(opts));
});
after(() => {
const w = remoteContext.shift();
w!.close();
});
}
export async function itremote (name: string, fn: Function, args?: any[]) {
it(name, async () => {
const w = await getRemoteContext();
const { ok, message } = await w.webContents.executeJavaScript(`(async () => {
try {
const chai_1 = require('chai')
chai_1.use(require('chai-as-promised'))
chai_1.use(require('dirty-chai'))
await (${fn})(...${JSON.stringify(args ?? [])})
return {ok: true};
} catch (e) {
return {ok: false, message: e.message}
}
})()`);
if (!ok) { throw new AssertionError(message); }
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,322 +0,0 @@
const { expect } = require('chai');
const fs = require('fs');
const http = require('http');
const path = require('path');
const url = require('url');
const ChildProcess = require('child_process');
const { ipcRenderer } = require('electron');
const { emittedOnce, waitForEvent } = require('./events-helpers');
const { ifit, ifdescribe, delay } = require('./spec-helpers');
const features = process._linkedBinding('electron_common_features');
/* Most of the APIs here don't use standard callbacks */
/* eslint-disable standard/no-callback-literal */
describe('chromium feature', () => {
const fixtures = path.resolve(__dirname, 'fixtures');
describe('window.open', () => {
it('inherit options of parent window', async () => {
const message = waitForEvent(window, 'message');
const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no');
const event = await message;
b.close();
const width = outerWidth;
const height = outerHeight;
expect(event.data).to.equal(`size: ${width} ${height}`);
});
// FIXME(zcbenz): This test is making the spec runner hang on exit on Windows.
ifit(process.platform !== 'win32')('disables node integration when it is disabled on the parent window', async () => {
const windowUrl = require('url').format({
pathname: `${fixtures}/pages/window-opener-no-node-integration.html`,
protocol: 'file',
query: {
p: `${fixtures}/pages/window-opener-node.html`
},
slashes: true
});
const message = waitForEvent(window, 'message');
const b = window.open(windowUrl, '', 'nodeIntegration=no,contextIsolation=no,show=no');
const event = await message;
b.close();
expect(event.data.isProcessGlobalUndefined).to.be.true();
});
it('disables the <webview> tag when it is disabled on the parent window', async () => {
const windowUrl = require('url').format({
pathname: `${fixtures}/pages/window-opener-no-webview-tag.html`,
protocol: 'file',
query: {
p: `${fixtures}/pages/window-opener-webview.html`
},
slashes: true
});
const message = waitForEvent(window, 'message');
const b = window.open(windowUrl, '', 'webviewTag=no,contextIsolation=no,nodeIntegration=yes,show=no');
const event = await message;
b.close();
expect(event.data.isWebViewGlobalUndefined).to.be.true();
});
it('does not override child options', async () => {
const size = {
width: 350,
height: 450
};
const message = waitForEvent(window, 'message');
const b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height);
const event = await message;
b.close();
expect(event.data).to.equal(`size: ${size.width} ${size.height}`);
});
it('throws an exception when the arguments cannot be converted to strings', () => {
expect(() => {
window.open('', { toString: null });
}).to.throw('Cannot convert object to primitive value');
expect(() => {
window.open('', '', { toString: 3 });
}).to.throw('Cannot convert object to primitive value');
});
it('does not throw an exception when the features include webPreferences', () => {
let b = null;
expect(() => {
b = window.open('', '', 'webPreferences=');
}).to.not.throw();
b.close();
});
});
describe('window.opener', () => {
it('is not null for window opened by window.open', async () => {
const message = waitForEvent(window, 'message');
const b = window.open(`file://${fixtures}/pages/window-opener.html`, '', 'show=no');
const event = await message;
b.close();
expect(event.data).to.equal('object');
});
});
describe('window.opener.postMessage', () => {
it('sets source and origin correctly', async () => {
const message = waitForEvent(window, 'message');
const b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no');
const event = await message;
try {
expect(event.source).to.deep.equal(b);
expect(event.origin).to.equal('file://');
} finally {
b.close();
}
});
it('supports windows opened from a <webview>', async () => {
const webview = new WebView();
const consoleMessage = waitForEvent(webview, 'console-message');
webview.allowpopups = true;
webview.setAttribute('webpreferences', 'contextIsolation=no');
webview.src = url.format({
pathname: `${fixtures}/pages/webview-opener-postMessage.html`,
protocol: 'file',
query: {
p: `${fixtures}/pages/window-opener-postMessage.html`
},
slashes: true
});
document.body.appendChild(webview);
const event = await consoleMessage;
webview.remove();
expect(event.message).to.equal('message');
});
describe('targetOrigin argument', () => {
let serverURL;
let server;
beforeEach((done) => {
server = http.createServer((req, res) => {
res.writeHead(200);
const filePath = path.join(fixtures, 'pages', 'window-opener-targetOrigin.html');
res.end(fs.readFileSync(filePath, 'utf8'));
});
server.listen(0, '127.0.0.1', () => {
serverURL = `http://127.0.0.1:${server.address().port}`;
done();
});
});
afterEach(() => {
server.close();
});
it('delivers messages that match the origin', async () => {
const message = waitForEvent(window, 'message');
const b = window.open(serverURL, '', 'show=no,contextIsolation=no,nodeIntegration=yes');
const event = await message;
b.close();
expect(event.data).to.equal('deliver');
});
});
});
describe('storage', () => {
describe('DOM storage quota increase', () => {
['localStorage', 'sessionStorage'].forEach((storageName) => {
const storage = window[storageName];
it(`allows saving at least 40MiB in ${storageName}`, async () => {
// Although JavaScript strings use UTF-16, the underlying
// storage provider may encode strings differently, muddling the
// translation between character and byte counts. However,
// a string of 40 * 2^20 characters will require at least 40MiB
// and presumably no more than 80MiB, a size guaranteed to
// to exceed the original 10MiB quota yet stay within the
// new 100MiB quota.
// Note that both the key name and value affect the total size.
const testKeyName = '_electronDOMStorageQuotaIncreasedTest';
const length = 40 * Math.pow(2, 20) - testKeyName.length;
storage.setItem(testKeyName, 'X'.repeat(length));
// Wait at least one turn of the event loop to help avoid false positives
// Although not entirely necessary, the previous version of this test case
// failed to detect a real problem (perhaps related to DOM storage data caching)
// wherein calling `getItem` immediately after `setItem` would appear to work
// but then later (e.g. next tick) it would not.
await delay(1);
try {
expect(storage.getItem(testKeyName)).to.have.lengthOf(length);
} finally {
storage.removeItem(testKeyName);
}
});
it(`throws when attempting to use more than 128MiB in ${storageName}`, () => {
expect(() => {
const testKeyName = '_electronDOMStorageQuotaStillEnforcedTest';
const length = 128 * Math.pow(2, 20) - testKeyName.length;
try {
storage.setItem(testKeyName, 'X'.repeat(length));
} finally {
storage.removeItem(testKeyName);
}
}).to.throw();
});
});
});
it('requesting persistent quota works', async () => {
const grantedBytes = await new Promise(resolve => {
navigator.webkitPersistentStorage.requestQuota(1024 * 1024, resolve);
});
expect(grantedBytes).to.equal(1048576);
});
});
describe('Promise', () => {
it('resolves correctly in Node.js calls', (done) => {
class XElement extends HTMLElement {}
customElements.define('x-element', XElement);
setImmediate(() => {
let called = false;
Promise.resolve().then(() => {
done(called ? undefined : new Error('wrong sequence'));
});
document.createElement('x-element');
called = true;
});
});
it('resolves correctly in Electron calls', (done) => {
class YElement extends HTMLElement {}
customElements.define('y-element', YElement);
ipcRenderer.invoke('ping').then(() => {
let called = false;
Promise.resolve().then(() => {
done(called ? undefined : new Error('wrong sequence'));
});
document.createElement('y-element');
called = true;
});
});
});
describe('window.alert(message, title)', () => {
it('throws an exception when the arguments cannot be converted to strings', () => {
expect(() => {
window.alert({ toString: null });
}).to.throw('Cannot convert object to primitive value');
});
});
describe('window.confirm(message, title)', () => {
it('throws an exception when the arguments cannot be converted to strings', () => {
expect(() => {
window.confirm({ toString: null }, 'title');
}).to.throw('Cannot convert object to primitive value');
});
});
describe('window.history', () => {
describe('window.history.go(offset)', () => {
it('throws an exception when the argument cannot be converted to a string', () => {
expect(() => {
window.history.go({ toString: null });
}).to.throw('Cannot convert object to primitive value');
});
});
});
// TODO(nornagon): this is broken on CI, it triggers:
// [FATAL:speech_synthesis.mojom-shared.h(237)] The outgoing message will
// trigger VALIDATION_ERROR_UNEXPECTED_NULL_POINTER at the receiving side
// (null text in SpeechSynthesisUtterance struct).
describe.skip('SpeechSynthesis', () => {
before(function () {
if (!features.isTtsEnabled()) {
this.skip();
}
});
it('should emit lifecycle events', async () => {
const sentence = `long sentence which will take at least a few seconds to
utter so that it's possible to pause and resume before the end`;
const utter = new SpeechSynthesisUtterance(sentence);
// Create a dummy utterance so that speech synthesis state
// is initialized for later calls.
speechSynthesis.speak(new SpeechSynthesisUtterance());
speechSynthesis.cancel();
speechSynthesis.speak(utter);
// paused state after speak()
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onstart = resolve; });
// paused state after start event
expect(speechSynthesis.paused).to.be.false();
speechSynthesis.pause();
// paused state changes async, right before the pause event
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onpause = resolve; });
expect(speechSynthesis.paused).to.be.true();
speechSynthesis.resume();
await new Promise((resolve) => { utter.onresume = resolve; });
// paused state after resume event
expect(speechSynthesis.paused).to.be.false();
await new Promise((resolve) => { utter.onend = resolve; });
});
});
});
describe('console functions', () => {
it('should exist', () => {
expect(console.log, 'log').to.be.a('function');
expect(console.error, 'error').to.be.a('function');
expect(console.warn, 'warn').to.be.a('function');
expect(console.info, 'info').to.be.a('function');
expect(console.debug, 'debug').to.be.a('function');
expect(console.trace, 'trace').to.be.a('function');
expect(console.time, 'time').to.be.a('function');
expect(console.timeEnd, 'timeEnd').to.be.a('function');
});
});

View File

@@ -11,11 +11,15 @@ app.whenReady().then(() => {
const lastArg = process.argv[process.argv.length - 1];
const client = net.connect(socketPath);
client.once('connect', () => {
client.end(String(lastArg === '--second'));
client.end(lastArg);
});
client.once('end', () => {
if (lastArg !== '--second') {
app.relaunch({ args: process.argv.slice(1).concat('--second') });
if (lastArg === '--first') {
// Once without execPath specified
app.relaunch({ args: process.argv.slice(1, -1).concat('--second') });
} else if (lastArg === '--second') {
// And once with execPath specified
app.relaunch({ execPath: process.argv[0], args: process.argv.slice(1, -1).concat('--third') });
}
app.exit(0);
});

View File

@@ -1,451 +0,0 @@
const ChildProcess = require('child_process');
const { expect } = require('chai');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { ipcRenderer } = require('electron');
const features = process._linkedBinding('electron_common_features');
const { emittedOnce } = require('./events-helpers');
const { ifit } = require('./spec-helpers');
describe('node feature', () => {
const fixtures = path.join(__dirname, 'fixtures');
describe('child_process', () => {
beforeEach(function () {
if (!features.isRunAsNodeEnabled()) {
this.skip();
}
});
describe('child_process.fork', () => {
it('works in current process', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'ping.js'));
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(msg).to.equal('message');
});
it('preserves args', async () => {
const args = ['--expose_gc', '-test', '1'];
const child = ChildProcess.fork(path.join(fixtures, 'module', 'process_args.js'), args);
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(args).to.deep.equal(msg.slice(2));
});
it('works in forked process', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'));
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(msg).to.equal('message');
});
it('works in forked process when options.env is specified', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'), [], {
path: process.env.PATH
});
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(msg).to.equal('message');
});
it('has String::localeCompare working in script', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'locale-compare.js'));
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(msg).to.deep.equal([0, -1, 1]);
});
it('has setImmediate working in script', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'set-immediate.js'));
const message = emittedOnce(child, 'message');
child.send('message');
const [msg] = await message;
expect(msg).to.equal('ok');
});
it('pipes stdio', async () => {
const child = ChildProcess.fork(path.join(fixtures, 'module', 'process-stdout.js'), { silent: true });
let data = '';
child.stdout.on('data', (chunk) => {
data += String(chunk);
});
const [code] = await emittedOnce(child, 'close');
expect(code).to.equal(0);
expect(data).to.equal('pipes stdio');
});
it('works when sending a message to a process forked with the --eval argument', async () => {
const source = "process.on('message', (message) => { process.send(message) })";
const forked = ChildProcess.fork('--eval', [source]);
const message = emittedOnce(forked, 'message');
forked.send('hello');
const [msg] = await message;
expect(msg).to.equal('hello');
});
it('has the electron version in process.versions', async () => {
const source = 'process.send(process.versions)';
const forked = ChildProcess.fork('--eval', [source]);
const [message] = await emittedOnce(forked, 'message');
expect(message)
.to.have.own.property('electron')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
});
});
describe('child_process.spawn', () => {
let child;
afterEach(() => {
if (child != null) child.kill();
});
it('supports spawning Electron as a node process via the ELECTRON_RUN_AS_NODE env var', async () => {
child = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'run-as-node.js')], {
env: {
ELECTRON_RUN_AS_NODE: true
}
});
let output = '';
child.stdout.on('data', data => {
output += data;
});
await emittedOnce(child.stdout, 'close');
expect(JSON.parse(output)).to.deep.equal({
stdoutType: 'pipe',
processType: 'undefined',
window: 'undefined'
});
});
});
describe('child_process.exec', () => {
(process.platform === 'linux' ? it : it.skip)('allows executing a setuid binary from non-sandboxed renderer', () => {
// Chrome uses prctl(2) to set the NO_NEW_PRIVILEGES flag on Linux (see
// https://github.com/torvalds/linux/blob/40fde647cc/Documentation/userspace-api/no_new_privs.rst).
// We disable this for unsandboxed processes, which the renderer tests
// are running in. If this test fails with an error like 'effective uid
// is not 0', then it's likely that our patch to prevent the flag from
// being set has become ineffective.
const stdout = ChildProcess.execSync('sudo --help');
expect(stdout).to.not.be.empty();
});
});
});
describe('contexts', () => {
describe('setTimeout in fs callback', () => {
it('does not crash', (done) => {
fs.readFile(__filename, () => {
setTimeout(done, 0);
});
});
});
describe('error thrown in renderer process node context', () => {
it('gets emitted as a process uncaughtException event', (done) => {
const error = new Error('boo!');
const listeners = process.listeners('uncaughtException');
process.removeAllListeners('uncaughtException');
process.on('uncaughtException', (thrown) => {
try {
expect(thrown).to.equal(error);
done();
} catch (e) {
done(e);
} finally {
process.removeAllListeners('uncaughtException');
listeners.forEach((listener) => process.on('uncaughtException', listener));
}
});
fs.readFile(__filename, () => {
throw error;
});
});
});
describe('URL handling in the renderer process', () => {
it('can successfully handle WHATWG URLs constructed by Blink', () => {
const url = new URL('file://' + path.resolve(fixtures, 'pages', 'base-page.html'));
expect(() => {
fs.createReadStream(url);
}).to.not.throw();
});
});
describe('error thrown in main process node context', () => {
it('gets emitted as a process uncaughtException event', () => {
const error = ipcRenderer.sendSync('handle-uncaught-exception', 'hello');
expect(error).to.equal('hello');
});
});
describe('promise rejection in main process node context', () => {
it('gets emitted as a process unhandledRejection event', () => {
const error = ipcRenderer.sendSync('handle-unhandled-rejection', 'hello');
expect(error).to.equal('hello');
});
});
describe('setTimeout called under blink env in renderer process', () => {
it('can be scheduled in time', (done) => {
setTimeout(done, 10);
});
it('works from the timers module', (done) => {
require('timers').setTimeout(done, 10);
});
});
describe('setInterval called under blink env in renderer process', () => {
it('can be scheduled in time', (done) => {
const id = setInterval(() => {
clearInterval(id);
done();
}, 10);
});
it('can be scheduled in time from timers module', (done) => {
const { setInterval, clearInterval } = require('timers');
const id = setInterval(() => {
clearInterval(id);
done();
}, 10);
});
});
});
describe('message loop', () => {
describe('process.nextTick', () => {
it('emits the callback', (done) => process.nextTick(done));
it('works in nested calls', (done) => {
process.nextTick(() => {
process.nextTick(() => process.nextTick(done));
});
});
});
describe('setImmediate', () => {
it('emits the callback', (done) => setImmediate(done));
it('works in nested calls', (done) => {
setImmediate(() => {
setImmediate(() => setImmediate(done));
});
});
});
});
describe('net.connect', () => {
before(function () {
if (!features.isRunAsNodeEnabled() || process.platform !== 'darwin') {
this.skip();
}
});
it('emit error when connect to a socket path without listeners', async () => {
const socketPath = path.join(os.tmpdir(), 'atom-shell-test.sock');
const script = path.join(fixtures, 'module', 'create_socket.js');
const child = ChildProcess.fork(script, [socketPath]);
const [code] = await emittedOnce(child, 'exit');
expect(code).to.equal(0);
const client = require('net').connect(socketPath);
const [error] = await emittedOnce(client, 'error');
expect(error.code).to.equal('ECONNREFUSED');
});
});
describe('Buffer', () => {
it('can be created from WebKit external string', () => {
const p = document.createElement('p');
p.innerText = '闲云潭影日悠悠,物换星移几度秋';
const b = Buffer.from(p.innerText);
expect(b.toString()).to.equal('闲云潭影日悠悠,物换星移几度秋');
expect(Buffer.byteLength(p.innerText)).to.equal(45);
});
it('correctly parses external one-byte UTF8 string', () => {
const p = document.createElement('p');
p.innerText = 'Jøhänñéß';
const b = Buffer.from(p.innerText);
expect(b.toString()).to.equal('Jøhänñéß');
expect(Buffer.byteLength(p.innerText)).to.equal(13);
});
it('does not crash when creating large Buffers', () => {
let buffer = Buffer.from(new Array(4096).join(' '));
expect(buffer.length).to.equal(4095);
buffer = Buffer.from(new Array(4097).join(' '));
expect(buffer.length).to.equal(4096);
});
it('does not crash for crypto operations', () => {
const crypto = require('crypto');
const data = 'lG9E+/g4JmRmedDAnihtBD4Dfaha/GFOjd+xUOQI05UtfVX3DjUXvrS98p7kZQwY3LNhdiFo7MY5rGft8yBuDhKuNNag9vRx/44IuClDhdQ=';
const key = 'q90K9yBqhWZnAMCMTOJfPQ==';
const cipherText = '{"error_code":114,"error_message":"Tham số không hợp lệ","data":null}';
for (let i = 0; i < 10000; ++i) {
const iv = Buffer.from('0'.repeat(32), 'hex');
const input = Buffer.from(data, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), iv);
const result = Buffer.concat([decipher.update(input), decipher.final()]).toString('utf8');
expect(cipherText).to.equal(result);
}
});
it('does not crash when using crypto.diffieHellman() constructors', () => {
const crypto = require('crypto');
crypto.createDiffieHellman('abc');
crypto.createDiffieHellman('abc', 2);
// Needed to test specific DiffieHellman ctors.
// eslint-disable-next-line no-octal
crypto.createDiffieHellman('abc', Buffer.from([02]));
// eslint-disable-next-line no-octal
crypto.createDiffieHellman('abc', '123');
});
it('does not crash when calling crypto.createPrivateKey() with an unsupported algorithm', () => {
const crypto = require('crypto');
const ed448 = {
crv: 'Ed448',
x: 'KYWcaDwgH77xdAwcbzOgvCVcGMy9I6prRQBhQTTdKXUcr-VquTz7Fd5adJO0wT2VHysF3bk3kBoA',
d: 'UhC3-vN5vp_g9PnTknXZgfXUez7Xvw-OfuJ0pYkuwzpYkcTvacqoFkV_O05WMHpyXkzH9q2wzx5n',
kty: 'OKP'
};
expect(() => {
crypto.createPrivateKey({ key: ed448, format: 'jwk' });
}).to.throw(/Invalid JWK data/);
});
});
describe('process.stdout', () => {
it('does not throw an exception when accessed', () => {
expect(() => process.stdout).to.not.throw();
});
it('does not throw an exception when calling write()', () => {
expect(() => {
process.stdout.write('test');
}).to.not.throw();
});
// TODO: figure out why process.stdout.isTTY is true on Darwin but not Linux/Win.
ifit(process.platform !== 'darwin')('isTTY should be undefined in the renderer process', function () {
expect(process.stdout.isTTY).to.be.undefined();
});
});
describe('process.stdin', () => {
it('does not throw an exception when accessed', () => {
expect(() => process.stdin).to.not.throw();
});
it('returns null when read from', () => {
expect(process.stdin.read()).to.be.null();
});
});
describe('process.version', () => {
it('should not have -pre', () => {
expect(process.version.endsWith('-pre')).to.be.false();
});
});
describe('vm.runInNewContext', () => {
it('should not crash', () => {
require('vm').runInNewContext('');
});
});
describe('crypto', () => {
it('should list the ripemd160 hash in getHashes', () => {
expect(require('crypto').getHashes()).to.include('ripemd160');
});
it('should be able to create a ripemd160 hash and use it', () => {
const hash = require('crypto').createHash('ripemd160');
hash.update('electron-ripemd160');
expect(hash.digest('hex')).to.equal('fa7fec13c624009ab126ebb99eda6525583395fe');
});
it('should list aes-{128,256}-cfb in getCiphers', () => {
expect(require('crypto').getCiphers()).to.include.members(['aes-128-cfb', 'aes-256-cfb']);
});
it('should be able to create an aes-128-cfb cipher', () => {
require('crypto').createCipheriv('aes-128-cfb', '0123456789abcdef', '0123456789abcdef');
});
it('should be able to create an aes-256-cfb cipher', () => {
require('crypto').createCipheriv('aes-256-cfb', '0123456789abcdef0123456789abcdef', '0123456789abcdef');
});
it('should be able to create a bf-{cbc,cfb,ecb} ciphers', () => {
require('crypto').createCipheriv('bf-cbc', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
require('crypto').createCipheriv('bf-cfb', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
require('crypto').createCipheriv('bf-ecb', Buffer.from('0123456789abcdef'), Buffer.from('01234567'));
});
it('should list des-ede-cbc in getCiphers', () => {
expect(require('crypto').getCiphers()).to.include('des-ede-cbc');
});
it('should be able to create an des-ede-cbc cipher', () => {
const key = Buffer.from('0123456789abcdeff1e0d3c2b5a49786', 'hex');
const iv = Buffer.from('fedcba9876543210', 'hex');
require('crypto').createCipheriv('des-ede-cbc', key, iv);
});
it('should not crash when getting an ECDH key', () => {
const ecdh = require('crypto').createECDH('prime256v1');
expect(ecdh.generateKeys()).to.be.an.instanceof(Buffer);
expect(ecdh.getPrivateKey()).to.be.an.instanceof(Buffer);
});
it('should not crash when generating DH keys or fetching DH fields', () => {
const dh = require('crypto').createDiffieHellman('modp15');
expect(dh.generateKeys()).to.be.an.instanceof(Buffer);
expect(dh.getPublicKey()).to.be.an.instanceof(Buffer);
expect(dh.getPrivateKey()).to.be.an.instanceof(Buffer);
expect(dh.getPrime()).to.be.an.instanceof(Buffer);
expect(dh.getGenerator()).to.be.an.instanceof(Buffer);
});
it('should not crash when creating an ECDH cipher', () => {
const crypto = require('crypto');
const dh = crypto.createECDH('prime256v1');
dh.generateKeys();
dh.setPrivateKey(dh.getPrivateKey());
});
});
it('includes the electron version in process.versions', () => {
expect(process.versions)
.to.have.own.property('electron')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+(\S*)?$/);
});
it('includes the chrome version in process.versions', () => {
expect(process.versions)
.to.have.own.property('chrome')
.that.is.a('string')
.and.matches(/^\d+\.\d+\.\d+\.\d+$/);
});
});

View File

@@ -147,39 +147,6 @@ app.whenReady().then(async function () {
});
});
ipcMain.on('prevent-next-will-attach-webview', (event) => {
event.sender.once('will-attach-webview', event => event.preventDefault());
});
ipcMain.on('break-next-will-attach-webview', (event, id) => {
event.sender.once('will-attach-webview', (event, webPreferences, params) => {
params.instanceId = null;
});
});
ipcMain.on('disable-node-on-next-will-attach-webview', (event, id) => {
event.sender.once('will-attach-webview', (event, webPreferences, params) => {
params.src = `file://${path.join(__dirname, '..', 'fixtures', 'pages', 'c.html')}`;
webPreferences.nodeIntegration = false;
});
});
ipcMain.on('disable-preload-on-next-will-attach-webview', (event, id) => {
event.sender.once('will-attach-webview', (event, webPreferences, params) => {
params.src = `file://${path.join(__dirname, '..', 'fixtures', 'pages', 'webview-stripped-preload.html')}`;
delete webPreferences.preload;
});
});
ipcMain.on('handle-uncaught-exception', (event, message) => {
suspendListeners(process, 'uncaughtException', (error) => {
event.returnValue = error.message;
});
fs.readFile(__filename, () => {
throw new Error(message);
});
});
ipcMain.on('handle-unhandled-rejection', (event, message) => {
suspendListeners(process, 'unhandledRejection', (error) => {
event.returnValue = error.message;

View File

@@ -174,11 +174,6 @@ webview.addEventListener('found-in-page', function (e) {
const requestId = webview.findInPage('test')
webview.addEventListener('new-window', async e => {
const { shell } = require('electron')
await shell.openExternal(e.url)
})
webview.addEventListener('close', function () {
webview.src = 'about:blank'
})

View File

@@ -33,28 +33,6 @@ describe('<webview> tag', function () {
return event.message;
};
async function loadFileInWebView (webview, attributes = {}) {
const thisFile = url.format({
pathname: __filename.replace(/\\/g, '/'),
protocol: 'file',
slashes: true
});
const src = `<script>
function loadFile() {
return new Promise((resolve) => {
fetch('${thisFile}').then(
() => resolve('loaded'),
() => resolve('failed')
)
});
}
console.log('ok');
</script>`;
attributes.src = `data:text/html;base64,${btoa(unescape(encodeURIComponent(src)))}`;
await startLoadingWebViewAndWaitForMessage(webview, attributes);
return await webview.executeJavaScript('loadFile()');
}
beforeEach(() => {
webview = new WebView();
});
@@ -66,636 +44,6 @@ describe('<webview> tag', function () {
webview.remove();
});
describe('src attribute', () => {
it('specifies the page to load', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/a.html`
});
expect(message).to.equal('a');
});
it('navigates to new page when changed', async () => {
await loadWebView(webview, {
src: `file://${fixtures}/pages/a.html`
});
webview.src = `file://${fixtures}/pages/b.html`;
const { message } = await waitForEvent(webview, 'console-message');
expect(message).to.equal('b');
});
it('resolves relative URLs', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: '../fixtures/pages/e.html'
});
expect(message).to.equal('Window script is loaded before preload script');
});
it('ignores empty values', () => {
expect(webview.src).to.equal('');
for (const emptyValue of ['', null, undefined]) {
webview.src = emptyValue;
expect(webview.src).to.equal('');
}
});
it('does not wait until loadURL is resolved', async () => {
await loadWebView(webview, { src: 'about:blank' });
const before = Date.now();
webview.src = 'https://github.com';
const now = Date.now();
// Setting src is essentially sending a sync IPC message, which should
// not exceed more than a few ms.
//
// This is for testing #18638.
expect(now - before).to.be.below(100);
});
});
describe('nodeintegration attribute', () => {
it('inserts no node symbols when not set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/c.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'undefined',
module: 'undefined',
process: 'undefined',
global: 'undefined'
});
});
it('inserts node symbols when set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/d.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object'
});
});
it('loads node symbols after POST navigation when set', async function () {
// FIXME Figure out why this is timing out on AppVeyor
if (process.env.APPVEYOR === 'True') {
this.skip();
return;
}
const message = await startLoadingWebViewAndWaitForMessage(webview, {
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/post.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object'
});
});
it('disables node integration on child windows when it is disabled on the webview', async () => {
const src = url.format({
pathname: `${fixtures}/pages/webview-opener-no-node-integration.html`,
protocol: 'file',
query: {
p: `${fixtures}/pages/window-opener-node.html`
},
slashes: true
});
loadWebView(webview, {
allowpopups: 'on',
webpreferences: 'contextIsolation=no',
src
});
const { message } = await waitForEvent(webview, 'console-message');
expect(JSON.parse(message).isProcessGlobalUndefined).to.be.true();
});
(nativeModulesEnabled ? it : it.skip)('loads native modules when navigation happens', async function () {
await loadWebView(webview, {
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/native-module.html`
});
webview.reload();
const { message } = await waitForEvent(webview, 'console-message');
expect(message).to.equal('function');
});
});
describe('preload attribute', () => {
it('loads the script before other scripts in window', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: `${fixtures}/module/preload.js`,
src: `file://${fixtures}/pages/e.html`,
contextIsolation: false
});
expect(message).to.be.a('string');
expect(message).to.be.not.equal('Window script is loaded before preload script');
});
it('preload script can still use "process" and "Buffer" when nodeintegration is off', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: `${fixtures}/module/preload-node-off.js`,
src: `file://${fixtures}/api/blank.html`
});
const types = JSON.parse(message);
expect(types).to.include({
process: 'object',
Buffer: 'function'
});
});
it('runs in the correct scope when sandboxed', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: `${fixtures}/module/preload-context.js`,
src: `file://${fixtures}/api/blank.html`,
webpreferences: 'sandbox=yes'
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function', // arguments passed to it should be available
electron: 'undefined', // objects from the scope it is called from should not be available
window: 'object', // the window object should be available
localVar: 'undefined' // but local variables should not be exposed to the window
});
});
it('preload script can require modules that still use "process" and "Buffer" when nodeintegration is off', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: `${fixtures}/module/preload-node-off-wrapper.js`,
webpreferences: 'sandbox=no',
src: `file://${fixtures}/api/blank.html`
});
const types = JSON.parse(message);
expect(types).to.include({
process: 'object',
Buffer: 'function'
});
});
it('receives ipc message in preload script', async () => {
await loadWebView(webview, {
preload: `${fixtures}/module/preload-ipc.js`,
src: `file://${fixtures}/pages/e.html`
});
const message = 'boom!';
webview.send('ping', message);
const { channel, args } = await waitForEvent(webview, 'ipc-message');
expect(channel).to.equal('pong');
expect(args).to.deep.equal([message]);
});
it('<webview>.sendToFrame()', async () => {
loadWebView(webview, {
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
preload: `${fixtures}/module/preload-ipc.js`,
src: `file://${fixtures}/pages/ipc-message.html`
});
const { frameId } = await waitForEvent(webview, 'ipc-message');
const message = 'boom!';
webview.sendToFrame(frameId, 'ping', message);
const { channel, args } = await waitForEvent(webview, 'ipc-message');
expect(channel).to.equal('pong');
expect(args).to.deep.equal([message]);
});
it('works without script tag in page', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: `${fixtures}/module/preload.js`,
webpreferences: 'sandbox=no',
src: `file://${fixtures}pages/base-page.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object',
Buffer: 'function'
});
});
it('resolves relative URLs', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
preload: '../fixtures/module/preload.js',
webpreferences: 'sandbox=no',
src: `file://${fixtures}/pages/e.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object',
Buffer: 'function'
});
});
it('ignores empty values', () => {
expect(webview.preload).to.equal('');
for (const emptyValue of ['', null, undefined]) {
webview.preload = emptyValue;
expect(webview.preload).to.equal('');
}
});
});
describe('httpreferrer attribute', () => {
it('sets the referrer url', (done) => {
const referrer = 'http://github.com/';
const server = http.createServer((req, res) => {
try {
expect(req.headers.referer).to.equal(referrer);
done();
} catch (e) {
done(e);
} finally {
res.end();
server.close();
}
}).listen(0, '127.0.0.1', () => {
const port = server.address().port;
loadWebView(webview, {
httpreferrer: referrer,
src: `http://127.0.0.1:${port}`
});
});
});
});
describe('useragent attribute', () => {
it('sets the user agent', async () => {
const referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko';
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/useragent.html`,
useragent: referrer
});
expect(message).to.equal(referrer);
});
});
describe('disablewebsecurity attribute', () => {
it('does not disable web security when not set', async () => {
const result = await loadFileInWebView(webview);
expect(result).to.equal('failed');
});
it('disables web security when set', async () => {
const result = await loadFileInWebView(webview, { disablewebsecurity: '' });
expect(result).to.equal('loaded');
});
it('does not break node integration', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
disablewebsecurity: '',
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/d.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object'
});
});
it('does not break preload script', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
disablewebsecurity: '',
preload: `${fixtures}/module/preload.js`,
webpreferences: 'sandbox=no',
src: `file://${fixtures}/pages/e.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object',
Buffer: 'function'
});
});
});
describe('partition attribute', () => {
it('inserts no node symbols when not set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
partition: 'test1',
src: `file://${fixtures}/pages/c.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'undefined',
module: 'undefined',
process: 'undefined',
global: 'undefined'
});
});
it('inserts node symbols when set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
nodeintegration: 'on',
partition: 'test2',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/d.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object'
});
});
it('isolates storage for different id', async () => {
window.localStorage.setItem('test', 'one');
const message = await startLoadingWebViewAndWaitForMessage(webview, {
partition: 'test3',
src: `file://${fixtures}/pages/partition/one.html`
});
const parsedMessage = JSON.parse(message);
expect(parsedMessage).to.include({
numberOfEntries: 0,
testValue: null
});
});
it('uses current session storage when no id is provided', async () => {
const testValue = 'one';
window.localStorage.setItem('test', testValue);
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/partition/one.html`
});
const parsedMessage = JSON.parse(message);
expect(parsedMessage).to.include({
numberOfEntries: 1,
testValue
});
});
});
describe('allowpopups attribute', () => {
const generateSpecs = (description, webpreferences = '') => {
describe(description, () => {
it('can not open new window when not set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
webpreferences,
src: `file://${fixtures}/pages/window-open-hide.html`
});
expect(message).to.equal('null');
});
it('can open new window when set', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
webpreferences,
allowpopups: 'on',
src: `file://${fixtures}/pages/window-open-hide.html`
});
expect(message).to.equal('window');
});
});
};
generateSpecs('without sandbox');
generateSpecs('with sandbox', 'sandbox=yes');
});
describe('webpreferences attribute', () => {
it('can enable nodeintegration', async () => {
const message = await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/d.html`,
webpreferences: 'nodeIntegration,contextIsolation=no'
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'function',
module: 'object',
process: 'object'
});
});
it('can disables web security and enable nodeintegration', async () => {
const result = await loadFileInWebView(webview, { webpreferences: 'webSecurity=no, nodeIntegration=yes, contextIsolation=no' });
expect(result).to.equal('loaded');
const type = await webview.executeJavaScript('typeof require');
expect(type).to.equal('function');
});
});
describe('new-window event', () => {
it('emits when window.open is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/window-open.html`,
allowpopups: true
});
const { url, frameName } = await waitForEvent(webview, 'new-window');
expect(url).to.equal('http://host/');
expect(frameName).to.equal('host');
});
it('emits when link with target is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/target-name.html`,
allowpopups: true
});
const { url, frameName } = await waitForEvent(webview, 'new-window');
expect(url).to.equal('http://host/');
expect(frameName).to.equal('target');
});
});
describe('ipc-message event', () => {
it('emits when guest sends an ipc message to browser', async () => {
loadWebView(webview, {
nodeintegration: 'on',
webpreferences: 'contextIsolation=no',
src: `file://${fixtures}/pages/ipc-message.html`
});
const { frameId, channel, args } = await waitForEvent(webview, 'ipc-message');
expect(frameId).to.be.an('array').that.has.lengthOf(2);
expect(channel).to.equal('channel');
expect(args).to.deep.equal(['arg1', 'arg2']);
});
});
describe('page-title-updated event', () => {
it('emits when title is set', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/a.html`
});
const { title, explicitSet } = await waitForEvent(webview, 'page-title-updated');
expect(title).to.equal('test');
expect(explicitSet).to.be.true();
});
});
describe('page-favicon-updated event', () => {
it('emits when favicon urls are received', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/a.html`
});
const { favicons } = await waitForEvent(webview, 'page-favicon-updated');
expect(favicons).to.be.an('array').of.length(2);
if (process.platform === 'win32') {
expect(favicons[0]).to.match(/^file:\/\/\/[A-Z]:\/favicon.png$/i);
} else {
expect(favicons[0]).to.equal('file:///favicon.png');
}
});
});
describe('did-redirect-navigation event', () => {
let server = null;
let uri = null;
before((done) => {
server = http.createServer((req, res) => {
if (req.url === '/302') {
res.setHeader('Location', '/200');
res.statusCode = 302;
res.end();
} else {
res.end();
}
});
server.listen(0, '127.0.0.1', () => {
uri = `http://127.0.0.1:${(server.address()).port}`;
done();
});
});
after(() => {
server.close();
});
it('is emitted on redirects', async () => {
loadWebView(webview, {
src: `${uri}/302`
});
const event = await waitForEvent(webview, 'did-redirect-navigation');
expect(event.url).to.equal(`${uri}/200`);
expect(event.isInPlace).to.be.false();
expect(event.isMainFrame).to.be.true();
expect(event.frameProcessId).to.be.a('number');
expect(event.frameRoutingId).to.be.a('number');
});
});
describe('will-navigate event', () => {
it('emits when a url that leads to outside of the page is clicked', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/webview-will-navigate.html`
});
const { url } = await waitForEvent(webview, 'will-navigate');
expect(url).to.equal('http://host/');
});
});
describe('did-navigate event', () => {
let p = path.join(fixtures, 'pages', 'webview-will-navigate.html');
p = p.replace(/\\/g, '/');
const pageUrl = url.format({
protocol: 'file',
slashes: true,
pathname: p
});
it('emits when a url that leads to outside of the page is clicked', async () => {
loadWebView(webview, { src: pageUrl });
const { url } = await waitForEvent(webview, 'did-navigate');
expect(url).to.equal(pageUrl);
});
});
describe('did-navigate-in-page event', () => {
it('emits when an anchor link is clicked', async () => {
let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html');
p = p.replace(/\\/g, '/');
const pageUrl = url.format({
protocol: 'file',
slashes: true,
pathname: p
});
loadWebView(webview, { src: pageUrl });
const event = await waitForEvent(webview, 'did-navigate-in-page');
expect(event.url).to.equal(`${pageUrl}#test_content`);
});
it('emits when window.history.replaceState is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/webview-did-navigate-in-page-with-history.html`
});
const { url } = await waitForEvent(webview, 'did-navigate-in-page');
expect(url).to.equal('http://host/');
});
it('emits when window.location.hash is changed', async () => {
let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html');
p = p.replace(/\\/g, '/');
const pageUrl = url.format({
protocol: 'file',
slashes: true,
pathname: p
});
loadWebView(webview, { src: pageUrl });
const event = await waitForEvent(webview, 'did-navigate-in-page');
expect(event.url).to.equal(`${pageUrl}#test`);
});
});
describe('close event', () => {
it('should fire when interior page calls window.close', async () => {
loadWebView(webview, { src: `file://${fixtures}/pages/close.html` });
await waitForEvent(webview, 'close');
});
});
// FIXME(zcbenz): Disabled because of moving to OOPIF webview.
xdescribe('setDevToolsWebContents() API', () => {
it('sets webContents of webview as devtools', async () => {
@@ -723,51 +71,6 @@ describe('<webview> tag', function () {
});
});
describe('devtools-opened event', () => {
it('should fire when webview.openDevTools() is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/base-page.html`
});
await waitForEvent(webview, 'dom-ready');
webview.openDevTools();
await waitForEvent(webview, 'devtools-opened');
webview.closeDevTools();
});
});
describe('devtools-closed event', () => {
it('should fire when webview.closeDevTools() is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/base-page.html`
});
await waitForEvent(webview, 'dom-ready');
webview.openDevTools();
await waitForEvent(webview, 'devtools-opened');
webview.closeDevTools();
await waitForEvent(webview, 'devtools-closed');
});
});
describe('devtools-focused event', () => {
it('should fire when webview.openDevTools() is called', async () => {
loadWebView(webview, {
src: `file://${fixtures}/pages/base-page.html`
});
const waitForDevToolsFocused = waitForEvent(webview, 'devtools-focused');
await waitForEvent(webview, 'dom-ready');
webview.openDevTools();
await waitForDevToolsFocused;
webview.closeDevTools();
});
});
describe('<webview>.reload()', () => {
it('should emit beforeunload handler', async () => {
await loadWebView(webview, {
@@ -891,28 +194,6 @@ describe('<webview> tag', function () {
});
});
describe('dom-ready event', () => {
it('emits when document is loaded', (done) => {
const server = http.createServer(() => {});
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
webview.addEventListener('dom-ready', () => {
done();
});
loadWebView(webview, {
src: `file://${fixtures}/pages/dom-ready.html?port=${port}`
});
});
});
it('throws a custom error when an API method is called before the event is emitted', () => {
const expectedErrorMessage =
'The WebView must be attached to the DOM ' +
'and the dom-ready event emitted before this method can be called.';
expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
});
});
describe('executeJavaScript', () => {
it('can return the result of the executed script', async () => {
await loadWebView(webview, {
@@ -985,26 +266,6 @@ describe('<webview> tag', function () {
});
});
describe('context-menu event', () => {
it('emits when right-clicked in page', async () => {
await loadWebView(webview, { src: 'about:blank' });
const promise = waitForEvent(webview, 'context-menu');
// Simulate right-click to create context-menu event.
const opts = { x: 0, y: 0, button: 'right' };
webview.sendInputEvent({ ...opts, type: 'mouseDown' });
webview.sendInputEvent({ ...opts, type: 'mouseUp' });
const { params } = await promise;
expect(params.pageURL).to.equal(webview.getURL());
expect(params.frame).to.be.undefined();
expect(params.x).to.be.a('number');
expect(params.y).to.be.a('number');
});
});
describe('media-started-playing media-paused events', () => {
beforeEach(function () {
if (!document.createElement('audio').canPlayType('audio/wav')) {
@@ -1031,39 +292,6 @@ describe('<webview> tag', function () {
});
});
describe('found-in-page event', () => {
it('emits when a request is made', async () => {
const didFinishLoad = waitForEvent(webview, 'did-finish-load');
loadWebView(webview, { src: `file://${fixtures}/pages/content.html` });
// TODO(deepak1556): With https://codereview.chromium.org/2836973002
// focus of the webContents is required when triggering the api.
// Remove this workaround after determining the cause for
// incorrect focus.
webview.focus();
await didFinishLoad;
const activeMatchOrdinal = [];
for (;;) {
const foundInPage = waitForEvent(webview, 'found-in-page');
const requestId = webview.findInPage('virtual');
const event = await foundInPage;
expect(event.result.requestId).to.equal(requestId);
expect(event.result.matches).to.equal(3);
activeMatchOrdinal.push(event.result.activeMatchOrdinal);
if (event.result.activeMatchOrdinal === event.result.matches) {
break;
}
}
expect(activeMatchOrdinal).to.deep.equal([1, 2, 3]);
webview.stopFindInPage('clearSelection');
});
});
describe('<webview>.getWebContentsId', () => {
it('can return the WebContents ID', async () => {
const src = 'about:blank';
@@ -1131,63 +359,6 @@ describe('<webview> tag', function () {
});
});
describe('will-attach-webview event', () => {
it('does not emit when src is not changed', async () => {
console.log('loadWebView(webview)');
loadWebView(webview);
await delay();
const expectedErrorMessage =
'The WebView must be attached to the DOM ' +
'and the dom-ready event emitted before this method can be called.';
expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
});
it('supports changing the web preferences', async () => {
ipcRenderer.send('disable-node-on-next-will-attach-webview');
const message = await startLoadingWebViewAndWaitForMessage(webview, {
nodeintegration: 'yes',
src: `file://${fixtures}/pages/a.html`
});
const types = JSON.parse(message);
expect(types).to.include({
require: 'undefined',
module: 'undefined',
process: 'undefined',
global: 'undefined'
});
});
it('handler modifying params.instanceId does not break <webview>', async () => {
ipcRenderer.send('break-next-will-attach-webview');
await startLoadingWebViewAndWaitForMessage(webview, {
src: `file://${fixtures}/pages/a.html`
});
});
it('supports preventing a webview from being created', async () => {
ipcRenderer.send('prevent-next-will-attach-webview');
loadWebView(webview, {
src: `file://${fixtures}/pages/c.html`
});
await waitForEvent(webview, 'destroyed');
});
it('supports removing the preload script', async () => {
ipcRenderer.send('disable-preload-on-next-will-attach-webview');
const message = await startLoadingWebViewAndWaitForMessage(webview, {
nodeintegration: 'yes',
preload: path.join(fixtures, 'module', 'preload-set-global.js'),
src: `file://${fixtures}/pages/a.html`
});
expect(message).to.equal('undefined');
});
});
describe('DOM events', () => {
let div;

View File

@@ -237,6 +237,7 @@ declare namespace NodeJS {
_linkedBinding(name: 'electron_browser_web_frame_main'): {
WebFrameMain: typeof Electron.WebFrameMain;
fromId(processId: number, routingId: number): Electron.WebFrameMain;
fromIdOrNull(processId: number, routingId: number): Electron.WebFrameMain;
}
_linkedBinding(name: 'electron_renderer_crash_reporter'): Electron.CrashReporter;
_linkedBinding(name: 'electron_renderer_ipc'): { ipc: IpcRendererBinding };

2717
yarn.lock

File diff suppressed because it is too large Load Diff