Compare commits

...

44 Commits

Author SHA1 Message Date
Electron Bot
39475f9404 Bump v8.0.0-nightly.20190903 2019-09-03 08:32:24 -07:00
Jeremy Apthorp
8589ab27a4 fix: strip chrome_sandbox executable (#20049) 2019-09-03 16:17:18 +09:00
Jeremy Apthorp
f537366387 test: move security warnings spec to main runner (#20055) 2019-09-03 16:02:22 +09:00
Jeremy Apthorp
d7161742d2 test: move content-script tests to main runner (#20056) 2019-09-03 16:01:42 +09:00
Jeremy Apthorp
6e88b6b445 test: move desktopCapturer spec to main runner (#20057) 2019-09-03 15:59:54 +09:00
Electron Bot
614079654c Bump v8.0.0-nightly.20190902 2019-09-02 08:34:32 -07:00
Electron Bot
51015c5b48 Bump v8.0.0-nightly.20190901 2019-09-01 08:31:00 -07:00
Samuel Attard
c621a36320 fix: ensure that the "top" coordinate of the inner frame is correct (#20051)
On multi-monitor setups where the monitors are not all origined at 0 on
the Y coordinate (E.g. vertical stacked monitors) the maximize
calculation was incorrect as it assumed top was "0".  This instead
adjusts the math to calculate the correct top value.
2019-08-30 15:45:59 -07:00
ipoint-pgerhard
e37ad09330 docs: Update boilerplates-and-clis.md (#19975)
Updated boilerplates-and-clis.md in order to more accuratly represent the current availability of templates in electron forge
2019-08-30 15:40:37 -05:00
Shelley Vohr
bfe256891c build: add gn-check to precommit linting (#19850) 2019-08-30 10:37:02 -07:00
Dave Jeffery
0f5ff1f5bb docs: clarify app.setName() effects (#19893)
* Document that `app.setName()` has no effect in Mac environments

* docs: add note to clarify `app.setName()` functionality

* Update app.md
2019-08-30 10:30:38 -07:00
Electron Bot
e96a042223 Bump v8.0.0-nightly.20190830 2019-08-30 08:32:04 -07:00
Shelley Vohr
5cbbd489d5 fix: honor cursor blink rate (#20020)
* fix: honor cursor blink rate on macOS

* fix: honor cursor blink rate on Linux

* fix: honor cursor blink rate on Windows

* refactor: clean up os_win cursor blink logic

* remove unneeded include
2019-08-30 09:39:46 -05:00
Jeremy Apthorp
805a55099b test: tsify more web contents specs (#19969)
* test: tsify more WebContents specs

* getFocusedWebContents

* setDevToolsWebContents, isFocused, isCurrentlyAudible

* getWebPreferences, openDevTools

* before-input-event

* zoom-changed

* sendInputEvent

* insertCSS

* startDrag

* focus, getOSProcessId

* zoom api

* more closeAllWindows

* fix detached dev tools test

* fix zoom-changed test

* compare the correct kind of id 🤦‍♂️

* 'fix' openDevTools test to wait for multiple focus events

* fix tests? 🤞

* use request instead of blur to detect openExternal success

* try not timing out the keychain for testing

* use blur event on mac, sigh

* oh, right, still gotta open an actual url
2019-08-29 19:45:41 -05:00
Samuel Attard
654338693f fix: Revert "fix: make sure that menu bar gets focus even when you click an item to focus it first (#19710)" (#20019)
This reverts commit 27b2747b61.
2019-08-29 15:06:39 -07:00
Shelley Vohr
a9e3dabc8a build: accidentally inverted a bool (#20029) 2019-08-29 11:11:10 -07:00
Jeremy Apthorp
609403fba6 test: tsify affinity spec (#19961) 2019-08-29 09:59:27 -07:00
Electron Bot
6b55584923 Bump v8.0.0-nightly.20190829 2019-08-29 08:31:27 -07:00
Shelley Vohr
81e6f317c9 chore: improve smoke test for tray (#19991) 2019-08-29 08:07:02 -07:00
Shelley Vohr
7d4e0ad7b0 build: simplify unicode console output (#20017) 2019-08-29 10:46:54 -04:00
Tomáš Hübelbauer
35ebbb5f6e docs: add a CSP meta tag to make the tutorial compliant with the security checklist (#19819)
I've asked #19775 because I was frustrated with how hard it was to find a way to fix (instead of hide) the CSP warning in Electron and I complained that even the official quick start guide wasn't compliant with the security checklist at https://electronjs.org/docs/tutorial/security. Someone helped me out with a CSP meta tag which I have later noticed is indeed mentioned in the checklist, too: https://electronjs.org/docs/tutorial/security#csp-meta-tag. I have not used the checklist one verbatim because it prevents a `script` tag from working when serving `index.html` through the `file:` protocol as the quick start does. I instead used the one the person in my issue recommended which seems to work well to me. I am not that well versed in CSP so there might be a better policy to include with the quick start, but this is what I've got for now.
2019-08-29 17:06:51 +09:00
Jeremy Apthorp
c819fbe852 test: move WebContentsView spec (#19990) 2019-08-29 16:17:44 +09:00
Alexey Kuzmin
c03288f458 chore: add missing includes (#20003) 2019-08-29 15:57:11 +09:00
Jeremy Apthorp
90d62e5b98 fix: nws13n: make ses.setUserAgent work (#20014)
* refactor tests to better control window creation

* fix: nws13n: make ses.setUserAgent work
2019-08-29 15:50:14 +09:00
Cheng Zhao
b3947d6a83 chore: cache URLLoaderFactory per-session (#19998)
* cache the URLLoaderFactory in AtomBrowserContext

* use cached loader factory in AtomURLLoaderFactory
2019-08-29 15:07:46 +09:00
Milan Burda
eed72c35d7 feat: add session.downloadURL() (#19889) 2019-08-28 20:27:20 -07:00
Jeremy Apthorp
79e936aea8 test: fix clearAuthCache test (#20015) 2019-08-28 17:43:12 -07:00
Milan Burda
01fdb80f7c refactor: implement isRemoteModuleEnabled via getLastWebPreferences() (#19220) 2019-08-28 15:57:03 -07:00
Shelley Vohr
04debd5890 build: add test runner parameter checks (#19994) 2019-08-28 15:19:52 -07:00
Jeremy Apthorp
af138dab55 test: move webRequest spec to main runner (#19992) 2019-08-28 13:56:15 -07:00
Jeremy Apthorp
f212ed85dd test: tsify sub-frames spec (#19965) 2019-08-28 13:55:01 -07:00
Jeremy Apthorp
99de0975c3 test: tsify powerMonitor spec (#19963) 2019-08-28 13:54:50 -07:00
Jeremy Apthorp
41d8247ffc test: tsify internal-spec (#19962) 2019-08-28 13:54:42 -07:00
Charles Kerr
217ed9aabc fix: gtk_init() called 2x in AtomBrowserMainParts (#19986)
Fixes #19984.
2019-08-28 12:36:03 -05:00
Shelley Vohr
538c4763cf chore: remove unused config files (#19997) 2019-08-28 09:18:49 -07:00
Electron Bot
3bc5302d78 Bump v8.0.0-nightly.20190828 2019-08-28 08:31:11 -07:00
Charles Kerr
987300c97a refactor: omit redundant map searches (#19929)
* refactor: don't walk maps twice to remove elements

* refactor: don't walk maps twice to read elements

* refactor: don't walk maps twice to insert elements

* refactor: don't walk map 3x on UvTaskRunner timeout

* refactor: more don't-walk-maps-twice cleanup

* fixup! refactor: don't walk maps twice to insert elements

* refactor: don't walk containers twice when erasing

* refactor: omit excess lookups in RemoteObjectFreer
2019-08-28 09:39:21 -05:00
Heilig Benedek
27ce6a9cd3 fix: handle WM_GETMINMAXINFO instead of letting chromium do it (#19928)
* fix: remove WM_GETMINMAXINFO workaround since it's no longer needed

* fix: handle WM_GETMINMAXINFO ourselves

* fix: remove part of the chromium WM_GETMINMAXINFO handler
2019-08-28 09:34:34 +09:00
Jeremy Apthorp
832c926712 fix: allow unsandboxed renderers to request new privileges (#19953)
* fix: allow unsandboxed renderers to request new privileges

* add test
2019-08-27 18:35:46 -04:00
Marat Abdullin
ae9424d93a feat: add "accessibleTitle" property to a BrowserWindow instance (#19698)
Sometimes it's necessary to convey more information about the window to screen reader users only (simply putting everything to the window title might be unnecessarily noisy).

For example, Chromium uses that technique to tell screen reader users that the window is in incognito mode (the incognito window looks differently and doesn't have «incognito» in the title, but for blind users the screen reader will announce that it's incognito).
2019-08-28 00:35:34 +02:00
Jeremy Apthorp
1dcda7b809 chore: DCHECK for correct thread in EventEmitter::EmitWithSender (#19959) 2019-08-27 15:15:52 -07:00
Jeremy Apthorp
bdc84d0bfb test: tsify session spec (#19604) 2019-08-27 14:55:19 -07:00
Jeremy Apthorp
4b8e1588b4 fix: remove unused header from extensions-only file (#19947) 2019-08-27 10:59:22 -07:00
Cheng Zhao
4eee71ffbf feat: migrate webRequest module to NetworkService (Part 9) (#19976)
* no need to get WebContents for URLLoaderFactory

* consult embedder for network_factory created in net module

* set disable_web_security to false

* re-enable webRequest tests in net module
2019-08-27 09:12:33 -07:00
95 changed files with 1979 additions and 1731 deletions

10
.github/main.workflow vendored
View File

@@ -1,10 +0,0 @@
workflow "Clerk" {
#TODO(codebytere): make this work properly on pull_request
on = "repository_dispatch"
resolves = "Check release notes"
}
action "Check release notes" {
uses = "electron/clerk@master"
secrets = [ "GITHUB_TOKEN" ]
}

25
.github/stale.yml vendored
View File

@@ -1,25 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 45
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- fixme/bug
- fixme/crash
- fixme/regression
- fixme/security
- blocked
- blocking-stable
- needs-review
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity and is not currently prioritized. It will be closed
in a week if no further activity occurs :)
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
If you still think this issue is relevant, please ping a maintainer or
leave a comment!

View File

@@ -1 +1 @@
8.0.0-nightly.20190827
8.0.0-nightly.20190903

View File

@@ -675,6 +675,8 @@ preferred over `name` by Electron.
Overrides the current application's name.
**Note:** This function overrides the name used internally by Electron; it does not affect the name that the OS uses.
**[Deprecated](modernization/property-updates.md)**
### `app.getLocale()`

View File

@@ -379,6 +379,9 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `disableHtmlFullscreenWindowResize` Boolean (optional) - Whether to
prevent the window from resizing when entering HTML Fullscreen. Default
is `false`.
* `accessibleTitle` String (optional) - An alternative title string provided only
to accessibility tools such as screen readers. This string is not directly
visible to users.
When setting minimum or maximum window size with `minWidth`/`maxWidth`/
`minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from
@@ -821,6 +824,12 @@ const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
```
#### `win.accessibleTitle`
A `String` property that defines an alternative title provided only to
accessibility tools such as screen readers. This string is not directly
visible to users.
### Instance Methods
Objects created with `new BrowserWindow` have the following instance methods:

View File

@@ -139,9 +139,9 @@ Writes any unwritten DOMStorage data to disk.
#### `ses.setProxy(config)`
* `config` Object
* `pacScript` String - The URL associated with the PAC file.
* `proxyRules` String - Rules indicating which proxies to use.
* `proxyBypassRules` String - Rules indicating which URLs should
* `pacScript` String (optional) - The URL associated with the PAC file.
* `proxyRules` String (optional) - Rules indicating which proxies to use.
* `proxyBypassRules` String (optional) - Rules indicating which URLs should
bypass the proxy settings.
Returns `Promise<void>` - Resolves when the proxy setting process is complete.
@@ -267,7 +267,7 @@ the original network configuration.
#### `ses.setCertificateVerifyProc(proc)`
* `proc` Function
* `proc` Function | null
* `request` Object
* `hostname` String
* `certificate` [Certificate](structures/certificate.md)
@@ -408,6 +408,17 @@ Returns `String` - The user agent for this session.
Returns `Promise<Buffer>` - resolves with blob data.
#### `ses.downloadURL(url)`
* `url` String
Initiates a download of the resource at `url`.
The API will generate a [DownloadItem](download-item.md) that can be accessed
with the [will-download](#event-will-download) event.
**Note:** This does not perform any security checks that relate to a page's origin,
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl).
#### `ses.createInterruptedDownload(options)`
* `options` Object
@@ -416,8 +427,8 @@ Returns `Promise<Buffer>` - resolves with blob data.
* `mimeType` String (optional)
* `offset` Integer - Start range for the download.
* `length` Integer - Total length of the download.
* `lastModified` String - Last-Modified header value.
* `eTag` String - ETag header value.
* `lastModified` String (optional) - Last-Modified header value.
* `eTag` String (optional) - ETag header value.
* `startTime` Double (optional) - Time when download was started in
number of seconds since UNIX epoch.

View File

@@ -1,6 +1,6 @@
# InputEvent Object
* `modifiers` String[] - An array of modifiers of the event, can
be `shift`, `control`, `alt`, `meta`, `isKeypad`, `isAutoRepeat`,
`leftButtonDown`, `middleButtonDown`, `rightButtonDown`, `capsLock`,
`numLock`, `left`, `right`.
* `modifiers` String[] (optional) - An array of modifiers of the event, can
be `shift`, `control`, `ctrl`, `alt`, `meta`, `command`, `cmd`, `isKeypad`,
`isAutoRepeat`, `leftButtonDown`, `middleButtonDown`, `rightButtonDown`,
`capsLock`, `numLock`, `left`, `right`.

View File

@@ -1593,8 +1593,8 @@ End subscribing for frame presentation events.
* `item` Object
* `file` String[] | String - The path(s) to the file(s) being dragged.
* `icon` [NativeImage](native-image.md) - The image must be non-empty on
macOS.
* `icon` [NativeImage](native-image.md) | String - The image must be
non-empty on macOS.
Sets the `item` as dragging item for current drag-drop operation, `file` is the
absolute path of the file to be dragged, and `icon` is the image showing under

View File

@@ -99,16 +99,16 @@ Some examples of valid `urls`:
* `timestamp` Double
* `requestHeaders` Record<string, string>
* `callback` Function
* `response` Object
* `beforeSendResponse` Object
* `cancel` Boolean (optional)
* `requestHeaders` Record<string, string> (optional) - When provided, request will be made
* `requestHeaders` Record<string, string | string[]> (optional) - When provided, request will be made
with these headers.
The `listener` will be called with `listener(details, callback)` before sending
an HTTP request, once the request headers are available. This may occur after a
TCP connection is made to the server, but before any http data is sent.
The `callback` has to be called with an `response` object.
The `callback` has to be called with a `response` object.
#### `webRequest.onSendHeaders([filter, ]listener)`
@@ -148,9 +148,9 @@ response are visible by the time this listener is fired.
* `statusCode` Integer
* `responseHeaders` Record<string, string> (optional)
* `callback` Function
* `response` Object
* `headersReceivedResponse` Object
* `cancel` Boolean (optional)
* `responseHeaders` Record<string, string> (optional) - When provided, the server is assumed
* `responseHeaders` Record<string, string | string[]> (optional) - When provided, the server is assumed
to have responded with these headers.
* `statusLine` String (optional) - Should be provided when overriding
`responseHeaders` to change header status otherwise original response
@@ -159,7 +159,7 @@ response are visible by the time this listener is fired.
The `listener` will be called with `listener(details, callback)` when HTTP
response headers of a request have been received.
The `callback` has to be called with an `response` object.
The `callback` has to be called with a `response` object.
#### `webRequest.onResponseStarted([filter, ]listener)`
@@ -201,6 +201,7 @@ and response headers are available.
* `timestamp` Double
* `redirectURL` String
* `statusCode` Integer
* `statusLine` String
* `ip` String (optional) - The server IP address that the request was
actually sent to.
* `fromCache` Boolean

View File

@@ -31,8 +31,7 @@ unifies the existing (and well maintained) build tools for Electron development
into a cohesive package so that anyone can jump right in to Electron
development.
Forge comes with [ready-to-use templates](https://electronforge.io/templates) for popular
frameworks like React, Vue, or Angular. It uses the same core modules used by the
Forge comes with [a ready-to-use template](https://electronforge.io/templates) using Webpack as a bundler. It includes an example typescript configuration and provides two configuration files to enable easy customization. It uses the same core modules used by the
greater Electron community (like [`electron-packager`](https://github.com/electron/electron-packager))  
changes made by Electron maintainers (like Slack) benefit Forge's users, too.

View File

@@ -195,6 +195,8 @@ Finally the `index.html` is the web page you want to show:
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
</head>
<body>
<h1>Hello World!</h1>

View File

@@ -511,7 +511,7 @@ no circumstances should you enable features speculatively.
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: ['ExecCommandInJavaScript']
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
```

View File

@@ -241,11 +241,16 @@ const unwrapArgs = function (sender, frameId, contextId, args) {
return args.map(metaToValue)
}
const isRemoteModuleEnabledImpl = function (contents) {
const webPreferences = contents.getLastWebPreferences() || {}
return !!webPreferences.enableRemoteModule
}
const isRemoteModuleEnabledCache = new WeakMap()
const isRemoteModuleEnabled = function (contents) {
if (!isRemoteModuleEnabledCache.has(contents)) {
isRemoteModuleEnabledCache.set(contents, contents._isRemoteModuleEnabled())
isRemoteModuleEnabledCache.set(contents, isRemoteModuleEnabledImpl(contents))
}
return isRemoteModuleEnabledCache.get(contents)

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "8.0.0-nightly.20190827",
"version": "8.0.0-nightly.20190903",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {
@@ -8,6 +8,7 @@
"@electron/typescript-definitions": "^8.6.1",
"@octokit/rest": "^16.3.2",
"@primer/octicons": "^9.1.1",
"@types/basic-auth": "^1.1.2",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0",
"@types/dirty-chai": "^2.0.0",
@@ -15,6 +16,7 @@
"@types/fs-extra": "^5.0.5",
"@types/mocha": "^5.2.6",
"@types/node": "^12.0.10",
"@types/send": "^0.14.5",
"@types/split": "^1.0.0",
"@types/webpack": "^4.4.32",
"@types/webpack-env": "^1.13.9",
@@ -77,6 +79,7 @@
"create-typescript-definitions": "npm run create-api-json && electron-typescript-definitions --api=electron-api.json && node spec/ts-smoke/runner.js",
"gn-typescript-definitions": "npm run create-typescript-definitions && shx cp electron.d.ts",
"pre-flight": "pre-flight",
"gn-check": "node ./script/gn-check.js",
"preinstall": "node -e 'process.exit(0)'",
"prepack": "check-for-leaks",
"repl": "node ./script/start.js --interactive",
@@ -113,6 +116,7 @@
"remark -qf"
],
"*.{gn,gni}": [
"npm run gn-check",
"python script/run-gn-format.py",
"git add"
],

View File

@@ -75,3 +75,5 @@ disable_color_correct_rendering.patch
add_contentgpuclient_precreatemessageloop_callback.patch
picture-in-picture.patch
disable_compositor_recycling.patch
allow_new_privileges_in_unsandboxed_child_processes.patch
expose_setuseragent_on_networkcontext.patch

View File

@@ -0,0 +1,30 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Apthorp <nornagon@nornagon.net>
Date: Mon, 26 Aug 2019 12:02:51 -0700
Subject: allow new privileges in unsandboxed child processes
This allows unsandboxed renderers to launch setuid processes on Linux.
diff --git a/content/browser/child_process_launcher_helper_linux.cc b/content/browser/child_process_launcher_helper_linux.cc
index 720b92a1a3a7ab5512f839005b272e4989d2ac65..b1759109627cd00053489dcdd397e942fa9d289f 100644
--- a/content/browser/child_process_launcher_helper_linux.cc
+++ b/content/browser/child_process_launcher_helper_linux.cc
@@ -54,6 +54,18 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
const int sandbox_fd = SandboxHostLinux::GetInstance()->GetChildSocket();
options->fds_to_remap.push_back(
std::make_pair(sandbox_fd, service_manager::GetSandboxFD()));
+
+ // (For Electron), if we're launching without zygote, that means we're
+ // launching an unsandboxed process (since all sandboxed processes are
+ // forked from the zygote). Relax the allow_new_privs option to permit
+ // launching suid processes from unsandboxed renderers.
+ service_manager::ZygoteHandle zygote_handle =
+ base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoZygote)
+ ? nullptr
+ : delegate_->GetZygote();
+ if (!zygote_handle) {
+ options->allow_new_privs = true;
+ }
}
options->environment = delegate_->GetEnvironment();

View File

@@ -0,0 +1,90 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Apthorp <nornagon@nornagon.net>
Date: Wed, 28 Aug 2019 12:21:25 -0700
Subject: expose SetUserAgent on NetworkContext
Applying
https://chromium-review.googlesource.com/c/chromium/src/+/1585083 before
it's merged. There may end up being a better way of doing this, or the
patch may be merged upstream, at which point this patch should be
removed.
diff --git a/net/url_request/static_http_user_agent_settings.h b/net/url_request/static_http_user_agent_settings.h
index 0ccfe130f00ec3b6c75cd8ee04d5a2777e1fd00c..653829457d58bf92057cc36aa8a289703d8b0577 100644
--- a/net/url_request/static_http_user_agent_settings.h
+++ b/net/url_request/static_http_user_agent_settings.h
@@ -26,13 +26,17 @@ class NET_EXPORT StaticHttpUserAgentSettings : public HttpUserAgentSettings {
accept_language_ = new_accept_language;
}
+ void set_user_agent(const std::string& new_user_agent) {
+ user_agent_ = new_user_agent;
+ }
+
// HttpUserAgentSettings implementation
std::string GetAcceptLanguage() const override;
std::string GetUserAgent() const override;
private:
std::string accept_language_;
- const std::string user_agent_;
+ std::string user_agent_;
DISALLOW_COPY_AND_ASSIGN(StaticHttpUserAgentSettings);
};
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 20243f6c333478f14e5bff3789e780b996887512..34e4fd20f78ee0376053720742fc9af60dae37e3 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -1095,6 +1095,13 @@ void NetworkContext::SetNetworkConditions(
std::move(network_conditions));
}
+void NetworkContext::SetUserAgent(const std::string& new_user_agent) {
+ // This may only be called on NetworkContexts created with a constructor that
+ // calls ApplyContextParamsToBuilder.
+ DCHECK(user_agent_settings_);
+ user_agent_settings_->set_user_agent(new_user_agent);
+}
+
void NetworkContext::SetAcceptLanguage(const std::string& new_accept_language) {
// This may only be called on NetworkContexts created with the constructor
// that calls MakeURLRequestContext().
diff --git a/services/network/network_context.h b/services/network/network_context.h
index f117ba5edfa7da25efd25e52ac5fff8b37ba3de3..5a805b4ee688ee7a113c833535db861b0e3b2ef9 100644
--- a/services/network/network_context.h
+++ b/services/network/network_context.h
@@ -213,6 +213,7 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkContext
void CloseIdleConnections(CloseIdleConnectionsCallback callback) override;
void SetNetworkConditions(const base::UnguessableToken& throttling_profile_id,
mojom::NetworkConditionsPtr conditions) override;
+ void SetUserAgent(const std::string& new_user_agent) override;
void SetAcceptLanguage(const std::string& new_accept_language) override;
void SetEnableReferrers(bool enable_referrers) override;
#if defined(OS_CHROMEOS)
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 034b5720ffa497429b2cdfcc06cd2d421f2db99c..aa2c88443e744396e381377ec20b03fb0e66d2db 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -857,6 +857,9 @@ interface NetworkContext {
SetNetworkConditions(mojo_base.mojom.UnguessableToken throttling_profile_id,
NetworkConditions? conditions);
+ // Updates the user agent to be used for requests.
+ SetUserAgent(string new_user_agent);
+
// Updates the Accept-Language header to be used for requests.
SetAcceptLanguage(string new_accept_language);
diff --git a/services/network/test/test_network_context.h b/services/network/test/test_network_context.h
index aa21b8e434c01a866e3fae04b8de54a3cdfb725e..63880702102a396576c5825f8c5c7731cde091cc 100644
--- a/services/network/test/test_network_context.h
+++ b/services/network/test/test_network_context.h
@@ -93,6 +93,7 @@ class TestNetworkContext : public mojom::NetworkContext {
void CloseIdleConnections(CloseIdleConnectionsCallback callback) override {}
void SetNetworkConditions(const base::UnguessableToken& throttling_profile_id,
mojom::NetworkConditionsPtr conditions) override {}
+ void SetUserAgent(const std::string& new_user_agent) override {}
void SetAcceptLanguage(const std::string& new_accept_language) override {}
void SetEnableReferrers(bool enable_referrers) override {}
#if defined(OS_CHROMEOS)

View File

@@ -7,8 +7,6 @@ security create-keychain -p $KEYCHAIN_PASSWORD $KEY_CHAIN
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD $KEY_CHAIN
# Set keychain locking timeout to 3600 seconds
security set-keychain-settings -t 3600 -u $KEY_CHAIN
# Add certificates to keychain and allow codesign to access them
security import "$(dirname $0)"/signing.cer -k $KEY_CHAIN -A /usr/bin/codesign
@@ -16,7 +14,7 @@ security import "$(dirname $0)"/signing.pem -k $KEY_CHAIN -A /usr/bin/codesign
security import "$(dirname $0)"/signing.p12 -k $KEY_CHAIN -P $SPEC_KEY_PASSWORD -A /usr/bin/codesign
echo "Add keychain to keychain-list"
security list-keychains -s mac-build.keychain
security list-keychains -s $KEY_CHAIN
echo "Setting key partition list"
security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASSWORD $KEY_CHAIN

34
script/gn-check.js Normal file
View File

@@ -0,0 +1,34 @@
const cp = require('child_process')
const path = require('path')
const { getOutDir } = require('./lib/utils')
const SOURCE_ROOT = path.normalize(path.dirname(__dirname))
const DEPOT_TOOLS = path.resolve(SOURCE_ROOT, '..', 'third_party', 'depot_tools')
const OUT_DIR = getOutDir()
if (!OUT_DIR) {
throw new Error(`No viable out dir: one of Debug, Testing, or Release must exist.`)
}
const env = Object.assign({
CHROMIUM_BUILDTOOLS_PATH: path.resolve(SOURCE_ROOT, '..', 'buildtools'),
DEPOT_TOOLS_WIN_TOOLCHAIN: '0'
}, process.env)
// Users may not have depot_tools in PATH.
env.PATH = `${env.PATH}${path.delimiter}${DEPOT_TOOLS}`
const gnCheckDirs = [
'//electron:electron_lib',
'//electron:electron_app',
'//electron:manifests',
'//electron/shell/common/api:mojo'
]
for (const dir of gnCheckDirs) {
const args = ['check', `../out/${OUT_DIR}`, dir]
const result = cp.spawnSync('gn', args, { env, stdio: 'inherit' })
if (result.status !== 0) process.exit(result.status)
}
process.exit(0)

View File

@@ -6,8 +6,8 @@ const ELECTRON_DIR = path.resolve(__dirname, '..', '..')
const SRC_DIR = path.resolve(ELECTRON_DIR, '..')
require('colors')
const pass = '\u2713'.green
const fail = '\u2717'.red
const pass = ''.green
const fail = ''.red
function getElectronExec () {
const OUT_DIR = getOutDir()

View File

@@ -19,8 +19,8 @@ const bumpType = args._[0]
const targetRepo = bumpType === 'nightly' ? 'nightlies' : 'electron'
require('colors')
const pass = '\u2713'.green
const fail = '\u2717'.red
const pass = ''.green
const fail = ''.red
if (!bumpType && !args.notesOnly) {
console.log(`Usage: prepare-release [stable | beta | nightly]` +

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env node
if (!process.env.CI) require('dotenv-safe').load()
require('colors')
const pass = '\u2713'.green
const fail = '\u2717'.red
const args = require('minimist')(process.argv.slice(2), {
string: ['tag', 'releaseID'],
default: { releaseID: '' }
})
const path = require('path')
const { execSync } = require('child_process')
const { GitProcess } = require('dugite')
const { getCurrentBranch, ELECTRON_DIR } = require('../lib/utils.js')
@@ -16,7 +14,9 @@ const octokit = require('@octokit/rest')({
auth: process.env.ELECTRON_GITHUB_TOKEN
})
const path = require('path')
require('colors')
const pass = '✓'.green
const fail = '✗'.red
function getLastBumpCommit (tag) {
const data = execSync(`git log -n1 --grep "Bump ${tag}" --format='format:{"hash": "%H", "message": "%s"}'`).toString()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
if (!process.env.CI) require('dotenv-safe').load()
require('colors')
const args = require('minimist')(process.argv.slice(2), {
boolean: [
'validateRelease',
@@ -17,13 +17,15 @@ const nugget = require('nugget')
const got = require('got')
const pkg = require('../../package.json')
const pkgVersion = `v${pkg.version}`
const pass = '\u2713'.green
const path = require('path')
const fail = '\u2717'.red
const sumchecker = require('sumchecker')
const temp = require('temp').track()
const { URL } = require('url')
require('colors')
const pass = '✓'.green
const fail = '✗'.red
const { ELECTRON_DIR } = require('../lib/utils')
const octokit = require('@octokit/rest')({

View File

@@ -7,6 +7,10 @@ const { hashElement } = require('folder-hash')
const path = require('path')
const unknownFlags = []
require('colors')
const pass = '✓'.green
const fail = '✗'.red
const args = require('minimist')(process.argv, {
string: ['runners'],
unknown: arg => unknownFlags.push(arg)
@@ -28,14 +32,23 @@ const BASE = path.resolve(__dirname, '../..')
const NPM_CMD = process.platform === 'win32' ? 'npm.cmd' : 'npm'
const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx'
const runners = new Map([
['main', { description: 'Main process specs', run: runMainProcessElectronTests }],
['remote', { description: 'Remote based specs', run: runRemoteBasedElectronTests }]
])
const specHashPath = path.resolve(__dirname, '../spec/.hash')
let runnersToRun = null
if (args.runners) {
runnersToRun = args.runners.split(',')
if (!runnersToRun.every(r => [...runners.keys()].includes(r))) {
console.log(`${fail} ${runnersToRun} must be a subset of [${[...runners.keys()].join(' | ')}]`)
process.exit(1)
}
console.log('Only running:', runnersToRun)
} else {
console.log('Will trigger all spec runners')
console.log(`Triggering both ${[...runners.keys()].join(' and ')} runners`)
}
async function main () {
@@ -79,10 +92,6 @@ function saveSpecHash ([newSpecHash, newSpecInstallHash]) {
async function runElectronTests () {
const errors = []
const runners = new Map([
['main', { description: 'Main process specs', run: runMainProcessElectronTests }],
['remote', { description: 'Remote based specs', run: runRemoteBasedElectronTests }]
])
const testResultsDir = process.env.ELECTRON_TEST_RESULTS_DIR
for (const [runnerId, { description, run }] of runners) {
@@ -106,7 +115,8 @@ async function runElectronTests () {
console.error('\n\nRunner Failed:', err[0])
console.error(err[1])
}
throw new Error('Electron test runners have failed')
console.log(`${fail} Electron test runners have failed`)
process.exit(1)
}
}
@@ -124,21 +134,30 @@ async function runRemoteBasedElectronTests () {
})
if (status !== 0) {
const textStatus = process.platform === 'win32' ? `0x${status.toString(16)}` : status.toString()
throw new Error(`Electron tests failed with code ${textStatus}.`)
console.log(`${fail} Electron tests failed with code ${textStatus}.`)
process.exit(1)
}
console.log(`${pass} Electron remote process tests passed.`)
}
async function runMainProcessElectronTests () {
const exe = path.resolve(BASE, utils.getElectronExec())
let exe = path.resolve(BASE, utils.getElectronExec())
const runnerArgs = ['electron/spec-main', ...unknownArgs.slice(2)]
if (process.platform === 'linux') {
runnerArgs.unshift(path.resolve(__dirname, 'dbus_mock.py'), exe)
exe = 'python'
}
const { status } = childProcess.spawnSync(exe, ['electron/spec-main', ...unknownArgs.slice(2)], {
const { status } = childProcess.spawnSync(exe, runnerArgs, {
cwd: path.resolve(__dirname, '../..'),
stdio: 'inherit'
})
if (status !== 0) {
const textStatus = process.platform === 'win32' ? `0x${status.toString(16)}` : status.toString()
throw new Error(`Electron tests failed with code ${textStatus}.`)
console.log(`${fail} Electron tests failed with code ${textStatus}.`)
process.exit(1)
}
console.log(`${pass} Electron main process tests passed.`)
}
async function installSpecModules () {
@@ -153,7 +172,8 @@ async function installSpecModules () {
stdio: 'inherit'
})
if (status !== 0 && !process.env.IGNORE_YARN_INSTALL_ERROR) {
throw new Error('Failed to yarn install in the spec folder')
console.log(`${fail} Failed to yarn install in the spec folder`)
process.exit(1)
}
}

View File

@@ -8,6 +8,7 @@ from lib.util import execute, get_out_dir
LINUX_BINARIES_TO_STRIP = [
'electron',
'chrome_sandbox',
'libffmpeg.so',
'libGLESv2.so',
'libEGL.so',

View File

@@ -42,12 +42,13 @@ bool UvTaskRunner::PostNonNestableDelayedTask(const base::Location& from_here,
// static
void UvTaskRunner::OnTimeout(uv_timer_t* timer) {
UvTaskRunner* self = static_cast<UvTaskRunner*>(timer->data);
if (!base::Contains(self->tasks_, timer))
auto& tasks = static_cast<UvTaskRunner*>(timer->data)->tasks_;
const auto iter = tasks.find(timer);
if (iter == std::end(tasks))
return;
std::move(self->tasks_[timer]).Run();
self->tasks_.erase(timer);
std::move(iter->second).Run();
tasks.erase(iter);
uv_timer_stop(timer);
uv_close(reinterpret_cast<uv_handle_t*>(timer), UvTaskRunner::OnClose);
}

View File

@@ -107,10 +107,9 @@ bool GlobalShortcut::Register(const ui::Accelerator& accelerator,
}
void GlobalShortcut::Unregister(const ui::Accelerator& accelerator) {
if (!base::Contains(accelerator_callback_map_, accelerator))
if (accelerator_callback_map_.erase(accelerator) == 0)
return;
accelerator_callback_map_.erase(accelerator);
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator,
this);
}

View File

@@ -176,21 +176,15 @@ void ProtocolNS::RegisterURLLoaderFactories(
ProtocolError ProtocolNS::RegisterProtocol(ProtocolType type,
const std::string& scheme,
const ProtocolHandler& handler) {
ProtocolError error = ProtocolError::OK;
if (!base::Contains(handlers_, scheme))
handlers_[scheme] = std::make_pair(type, handler);
else
error = ProtocolError::REGISTERED;
return error;
const bool added = base::TryEmplace(handlers_, scheme, type, handler).second;
return added ? ProtocolError::OK : ProtocolError::REGISTERED;
}
void ProtocolNS::UnregisterProtocol(const std::string& scheme,
mate::Arguments* args) {
ProtocolError error = ProtocolError::OK;
if (base::Contains(handlers_, scheme))
handlers_.erase(scheme);
else
error = ProtocolError::NOT_REGISTERED;
const bool removed = handlers_.erase(scheme) != 0;
const auto error =
removed ? ProtocolError::OK : ProtocolError::NOT_REGISTERED;
HandleOptionalCallback(args, error);
}
@@ -201,21 +195,16 @@ bool ProtocolNS::IsProtocolRegistered(const std::string& scheme) {
ProtocolError ProtocolNS::InterceptProtocol(ProtocolType type,
const std::string& scheme,
const ProtocolHandler& handler) {
ProtocolError error = ProtocolError::OK;
if (!base::Contains(intercept_handlers_, scheme))
intercept_handlers_[scheme] = std::make_pair(type, handler);
else
error = ProtocolError::INTERCEPTED;
return error;
const bool added =
base::TryEmplace(intercept_handlers_, scheme, type, handler).second;
return added ? ProtocolError::OK : ProtocolError::INTERCEPTED;
}
void ProtocolNS::UninterceptProtocol(const std::string& scheme,
mate::Arguments* args) {
ProtocolError error = ProtocolError::OK;
if (base::Contains(intercept_handlers_, scheme))
intercept_handlers_.erase(scheme);
else
error = ProtocolError::NOT_INTERCEPTED;
const bool removed = intercept_handlers_.erase(scheme) != 0;
const auto error =
removed ? ProtocolError::OK : ProtocolError::NOT_INTERCEPTED;
HandleOptionalCallback(args, error);
}

View File

@@ -19,6 +19,7 @@
#include "chrome/browser/browser_process.h"
#include "chrome/common/pref_names.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/value_map_pref_store.h"
#include "components/proxy_config/proxy_config_dictionary.h"
@@ -489,8 +490,10 @@ void Session::AllowNTLMCredentialsForDomains(const std::string& domains) {
void Session::SetUserAgent(const std::string& user_agent,
mate::Arguments* args) {
CHECK(false)
<< "TODO: This was disabled when the network service was turned on";
browser_context_->SetUserAgent(user_agent);
content::BrowserContext::GetDefaultStoragePartition(browser_context_.get())
->GetNetworkContext()
->SetUserAgent(user_agent);
}
std::string Session::GetUserAgent() {
@@ -510,6 +513,14 @@ v8::Local<v8::Promise> Session::GetBlobData(v8::Isolate* isolate,
return handle;
}
void Session::DownloadURL(const GURL& url) {
auto* download_manager =
content::BrowserContext::GetDownloadManager(browser_context());
auto download_params = std::make_unique<download::DownloadUrlParameters>(
url, MISSING_TRAFFIC_ANNOTATION);
download_manager->DownloadUrl(std::move(download_params));
}
void Session::CreateInterruptedDownload(const mate::Dictionary& options) {
int64_t offset = 0, length = 0;
double start_time = 0.0;
@@ -694,6 +705,7 @@ void Session::BuildPrototype(v8::Isolate* isolate,
.SetMethod("setUserAgent", &Session::SetUserAgent)
.SetMethod("getUserAgent", &Session::GetUserAgent)
.SetMethod("getBlobData", &Session::GetBlobData)
.SetMethod("downloadURL", &Session::DownloadURL)
.SetMethod("createInterruptedDownload",
&Session::CreateInterruptedDownload)
.SetMethod("setPreloads", &Session::SetPreloads)

View File

@@ -79,6 +79,7 @@ class Session : public mate::TrackableObject<Session>,
std::string GetUserAgent();
v8::Local<v8::Promise> GetBlobData(v8::Isolate* isolate,
const std::string& uuid);
void DownloadURL(const GURL& url);
void CreateInterruptedDownload(const mate::Dictionary& options);
void SetPreloads(const std::vector<base::FilePath::StringType>& preloads);
std::vector<base::FilePath::StringType> GetPreloads() const;

View File

@@ -575,6 +575,14 @@ std::string TopLevelWindow::GetTitle() {
return window_->GetTitle();
}
void TopLevelWindow::SetAccessibleTitle(const std::string& title) {
window_->SetAccessibleTitle(title);
}
std::string TopLevelWindow::GetAccessibleTitle() {
return window_->GetAccessibleTitle();
}
void TopLevelWindow::FlashFrame(bool flash) {
window_->FlashFrame(flash);
}
@@ -964,9 +972,6 @@ bool TopLevelWindow::HookWindowMessage(UINT message,
}
void TopLevelWindow::UnhookWindowMessage(UINT message) {
if (!base::Contains(messages_callback_map_, message))
return;
messages_callback_map_.erase(message);
}
@@ -1124,6 +1129,8 @@ void TopLevelWindow::BuildPrototype(v8::Isolate* isolate,
.SetMethod("getPosition", &TopLevelWindow::GetPosition)
.SetMethod("setTitle", &TopLevelWindow::SetTitle)
.SetMethod("getTitle", &TopLevelWindow::GetTitle)
.SetProperty("accessibleTitle", &TopLevelWindow::GetAccessibleTitle,
&TopLevelWindow::SetAccessibleTitle)
.SetMethod("flashFrame", &TopLevelWindow::FlashFrame)
.SetMethod("setSkipTaskbar", &TopLevelWindow::SetSkipTaskbar)
.SetMethod("setSimpleFullScreen", &TopLevelWindow::SetSimpleFullScreen)

View File

@@ -145,6 +145,8 @@ class TopLevelWindow : public mate::TrackableObject<TopLevelWindow>,
std::vector<int> GetPosition();
void SetTitle(const std::string& title);
std::string GetTitle();
void SetAccessibleTitle(const std::string& title);
std::string GetAccessibleTitle();
void FlashFrame(bool flash);
void SetSkipTaskbar(bool skip);
void SetExcludedFromShownWindowsMenu(bool excluded);

View File

@@ -6,7 +6,6 @@
#include <utility>
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "native_mate/dictionary.h"
#include "native_mate/object_template_builder.h"
@@ -179,10 +178,7 @@ URLRequestNS::URLRequestNS(mate::Arguments* args) : weak_factory_(this) {
session = Session::FromPartition(args->isolate(), "");
}
auto* browser_context = session->browser_context();
url_loader_factory_ =
content::BrowserContext::GetDefaultStoragePartition(browser_context)
->GetURLLoaderFactoryForBrowserProcess();
url_loader_factory_ = session->browser_context()->GetURLLoaderFactory();
InitWith(args->isolate(), args->GetThis());
}

View File

@@ -100,6 +100,12 @@
#if !defined(OS_MACOSX)
#include "ui/aura/window.h"
#else
#include "ui/base/cocoa/defaults_utils.h"
#endif
#if defined(OS_LINUX)
#include "ui/views/linux_ui/linux_ui.h"
#endif
#if defined(OS_LINUX) || defined(OS_WIN)
@@ -458,6 +464,25 @@ void WebContents::InitWithSessionAndOptions(
prefs->subpixel_rendering = params->subpixel_rendering;
#endif
// Honor the system's cursor blink rate settings
#if defined(OS_MACOSX)
base::TimeDelta interval;
if (ui::TextInsertionCaretBlinkPeriod(&interval))
prefs->caret_blink_interval = interval;
#elif defined(OS_LINUX)
views::LinuxUI* linux_ui = views::LinuxUI::instance();
if (linux_ui)
prefs->caret_blink_interval = linux_ui->GetCursorBlinkInterval();
#elif defined(OS_WIN)
const auto system_msec = ::GetCaretBlinkTime();
if (system_msec != 0) {
prefs->caret_blink_interval =
(system_msec == INFINITE)
? base::TimeDelta()
: base::TimeDelta::FromMilliseconds(system_msec);
}
#endif
// Save the preferences in C++.
new WebContentsPreferences(web_contents(), options);
@@ -2235,16 +2260,6 @@ v8::Local<v8::Value> WebContents::GetLastWebPreferences(
return mate::ConvertToV8(isolate, *web_preferences->last_preference());
}
bool WebContents::IsRemoteModuleEnabled() const {
if (web_contents()->GetVisibleURL().SchemeIs("devtools")) {
return false;
}
if (auto* web_preferences = WebContentsPreferences::From(web_contents())) {
return web_preferences->IsRemoteModuleEnabled();
}
return true;
}
v8::Local<v8::Value> WebContents::GetOwnerBrowserWindow() const {
if (owner_window())
return BrowserWindow::From(isolate(), owner_window());
@@ -2453,7 +2468,6 @@ void WebContents::BuildPrototype(v8::Isolate* isolate,
.SetMethod("_getPreloadPaths", &WebContents::GetPreloadPaths)
.SetMethod("getWebPreferences", &WebContents::GetWebPreferences)
.SetMethod("getLastWebPreferences", &WebContents::GetLastWebPreferences)
.SetMethod("_isRemoteModuleEnabled", &WebContents::IsRemoteModuleEnabled)
.SetMethod("getOwnerBrowserWindow", &WebContents::GetOwnerBrowserWindow)
.SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker)
.SetMethod("inspectSharedWorker", &WebContents::InspectSharedWorker)

View File

@@ -291,8 +291,6 @@ class WebContents : public mate::TrackableObject<WebContents>,
v8::Local<v8::Value> GetWebPreferences(v8::Isolate* isolate) const;
v8::Local<v8::Value> GetLastWebPreferences(v8::Isolate* isolate) const;
bool IsRemoteModuleEnabled() const;
// Returns the owner window.
v8::Local<v8::Value> GetOwnerBrowserWindow() const;

View File

@@ -396,10 +396,11 @@ template <typename... Args>
void WebRequestNS::HandleSimpleEvent(SimpleEvent event,
extensions::WebRequestInfo* request_info,
Args... args) {
if (!base::Contains(simple_listeners_, event))
const auto iter = simple_listeners_.find(event);
if (iter == std::end(simple_listeners_))
return;
const auto& info = simple_listeners_[event];
const auto& info = iter->second;
if (!MatchesFilterCondition(request_info, info.url_patterns))
return;
@@ -416,10 +417,11 @@ int WebRequestNS::HandleResponseEvent(ResponseEvent event,
net::CompletionOnceCallback callback,
Out out,
Args... args) {
if (!base::Contains(response_listeners_, event))
const auto iter = response_listeners_.find(event);
if (iter == std::end(response_listeners_))
return net::OK;
const auto& info = response_listeners_[event];
const auto& info = iter->second;
if (!MatchesFilterCondition(request_info, info.url_patterns))
return net::OK;
@@ -441,7 +443,8 @@ template <typename T>
void WebRequestNS::OnListenerResult(uint64_t id,
T out,
v8::Local<v8::Value> response) {
if (!base::Contains(callbacks_, id))
const auto iter = callbacks_.find(id);
if (iter == std::end(callbacks_))
return;
int result = net::OK;
@@ -461,7 +464,7 @@ void WebRequestNS::OnListenerResult(uint64_t id,
// asynchronously, because it used to work on IO thread before NetworkService.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callbacks_[id]), result));
callbacks_.erase(id);
callbacks_.erase(iter);
}
// static

View File

@@ -85,6 +85,7 @@ class EventEmitter : public Wrappable<T> {
base::Optional<electron::mojom::ElectronBrowser::MessageSyncCallback>
callback,
Args&&... args) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
v8::Locker locker(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Object> wrapper = GetWrapper();

View File

@@ -197,8 +197,9 @@ content::WebContents* AtomBrowserClient::GetWebContentsFromProcessID(
int process_id) {
// If the process is a pending process, we should use the web contents
// for the frame host passed into RegisterPendingProcess.
if (base::Contains(pending_processes_, process_id))
return pending_processes_[process_id];
const auto iter = pending_processes_.find(process_id);
if (iter != std::end(pending_processes_))
return iter->second;
// Certain render process will be created with no associated render view,
// for example: ServiceWorker.
@@ -561,9 +562,6 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches(
content::WebContents* web_contents = GetWebContentsFromProcessID(process_id);
if (web_contents) {
if (web_contents->GetVisibleURL().SchemeIs("devtools")) {
command_line->AppendSwitch(switches::kDisableRemoteModule);
}
auto* web_preferences = WebContentsPreferences::From(web_contents);
if (web_preferences)
web_preferences->AppendCommandLineSwitches(
@@ -988,18 +986,11 @@ bool AtomBrowserClient::WillCreateURLLoaderFactory(
mojo::PendingReceiver<network::mojom::URLLoaderFactory>* factory_receiver,
network::mojom::TrustedURLLoaderHeaderClientPtrInfo* header_client,
bool* bypass_redirect_checks) {
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(frame_host);
// For service workers there might be no WebContents.
if (!web_contents)
return false;
v8::Isolate* isolate = v8::Isolate::GetCurrent();
api::ProtocolNS* protocol = api::ProtocolNS::FromWrappedClass(
isolate, web_contents->GetBrowserContext());
api::ProtocolNS* protocol =
api::ProtocolNS::FromWrappedClass(isolate, browser_context);
DCHECK(protocol);
auto web_request = api::WebRequestNS::FromOrCreate(
isolate, web_contents->GetBrowserContext());
auto web_request = api::WebRequestNS::FromOrCreate(isolate, browser_context);
DCHECK(web_request.get());
auto proxied_receiver = std::move(*factory_receiver);

View File

@@ -28,6 +28,7 @@
#include "content/public/browser/storage_partition.h"
#include "net/base/escape.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "shell/browser/atom_blob_reader.h"
#include "shell/browser/atom_browser_client.h"
#include "shell/browser/atom_browser_main_parts.h"
@@ -271,6 +272,43 @@ predictors::PreconnectManager* AtomBrowserContext::GetPreconnectManager() {
return preconnect_manager_.get();
}
scoped_refptr<network::SharedURLLoaderFactory>
AtomBrowserContext::GetURLLoaderFactory() {
if (url_loader_factory_)
return url_loader_factory_;
network::mojom::URLLoaderFactoryPtr network_factory;
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_request =
mojo::MakeRequest(&network_factory);
// Consult the embedder.
network::mojom::TrustedURLLoaderHeaderClientPtrInfo header_client;
static_cast<content::ContentBrowserClient*>(AtomBrowserClient::Get())
->WillCreateURLLoaderFactory(
this, nullptr, -1,
content::ContentBrowserClient::URLLoaderFactoryType::kNavigation,
url::Origin(), &factory_request, &header_client, nullptr);
network::mojom::URLLoaderFactoryParamsPtr params =
network::mojom::URLLoaderFactoryParams::New();
params->header_client = std::move(header_client);
params->process_id = network::mojom::kBrowserProcessId;
params->is_trusted = true;
params->is_corb_enabled = false;
// The tests of net module would fail if this setting is true, it seems that
// the non-NetworkService implementation always has web security enabled.
params->disable_web_security = false;
auto* storage_partition =
content::BrowserContext::GetDefaultStoragePartition(this);
storage_partition->GetNetworkContext()->CreateURLLoaderFactory(
std::move(factory_request), std::move(params));
url_loader_factory_ =
base::MakeRefCounted<network::WrapperSharedURLLoaderFactory>(
std::move(network_factory));
return url_loader_factory_;
}
content::PushMessagingService* AtomBrowserContext::GetPushMessagingService() {
return nullptr;
}

View File

@@ -17,12 +17,17 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/resource_context.h"
#include "electron/buildflags/buildflags.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "shell/browser/media/media_device_id_salt.h"
class PrefRegistrySimple;
class PrefService;
class ValueMapPrefStore;
namespace network {
class SharedURLLoaderFactory;
}
namespace storage {
class SpecialStoragePolicy;
}
@@ -87,8 +92,8 @@ class AtomBrowserContext
int GetMaxCacheSize() const;
AtomBlobReader* GetBlobReader();
ResolveProxyHelper* GetResolveProxyHelper();
predictors::PreconnectManager* GetPreconnectManager();
scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory();
// content::BrowserContext:
base::FilePath GetPath() override;
@@ -180,6 +185,9 @@ class AtomBrowserContext
extensions::AtomExtensionSystem* extension_system_;
#endif
// Shared URLLoaderFactory.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
base::WeakPtrFactory<AtomBrowserContext> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(AtomBrowserContext);

View File

@@ -460,10 +460,6 @@ void AtomBrowserMainParts::PreMainMessageLoopRun() {
if (command_line->HasSwitch(switches::kRemoteDebuggingPort))
DevToolsManagerDelegate::StartHttpHandler();
#if defined(USE_X11)
libgtkui::GtkInitFromCommandLine(*base::CommandLine::ForCurrentProcess());
#endif
#if !defined(OS_MACOSX)
// The corresponding call in macOS is in AtomApplicationDelegate.
Browser::Get()->WillFinishLaunching();

View File

@@ -15,7 +15,6 @@
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/resource_request_info.h"
#include "content/public/common/user_agent.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/component_extension_resource_manager.h"

View File

@@ -9,6 +9,8 @@
#include <utility>
#include <vector>
#include "base/memory/ptr_util.h"
#include "base/strings/utf_string_conversions.h"
#include "native_mate/dictionary.h"
#include "shell/browser/browser.h"
#include "shell/browser/window_list.h"
@@ -582,6 +584,22 @@ const views::Widget* NativeWindow::GetWidget() const {
return widget();
}
base::string16 NativeWindow::GetAccessibleWindowTitle() const {
if (accessible_title_.empty()) {
return views::WidgetDelegate::GetAccessibleWindowTitle();
}
return accessible_title_;
}
void NativeWindow::SetAccessibleTitle(const std::string& title) {
accessible_title_ = base::UTF8ToUTF16(title);
}
std::string NativeWindow::GetAccessibleTitle() {
return base::UTF16ToUTF8(accessible_title_);
}
// static
void NativeWindowRelay::CreateForWebContents(
content::WebContents* web_contents,

View File

@@ -14,7 +14,9 @@
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/strings/string16.h"
#include "base/supports_user_data.h"
#include "base/values.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/web_contents_user_data.h"
#include "extensions/browser/app_window/size_constraints.h"
@@ -134,6 +136,11 @@ class NativeWindow : public base::SupportsUserData,
virtual void Invalidate() = 0;
virtual void SetTitle(const std::string& title) = 0;
virtual std::string GetTitle() = 0;
// Ability to augment the window title for the screen readers.
void SetAccessibleTitle(const std::string& title);
std::string GetAccessibleTitle();
virtual void FlashFrame(bool flash) = 0;
virtual void SetSkipTaskbar(bool skip) = 0;
virtual void SetExcludedFromShownWindowsMenu(bool excluded) = 0;
@@ -301,6 +308,7 @@ class NativeWindow : public base::SupportsUserData,
// views::WidgetDelegate:
views::Widget* GetWidget() override;
const views::Widget* GetWidget() const override;
base::string16 GetAccessibleWindowTitle() const override;
void set_content_view(views::View* view) { content_view_ = view; }
@@ -355,6 +363,9 @@ class NativeWindow : public base::SupportsUserData,
// Observers of this window.
base::ObserverList<NativeWindowObserver> observers_;
// Accessible title.
base::string16 accessible_title_;
base::WeakPtrFactory<NativeWindow> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(NativeWindow);

View File

@@ -345,24 +345,35 @@ bool NativeWindowViews::PreHandleMSG(UINT message,
return false;
}
case WM_GETMINMAXINFO: {
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
// We need to handle GETMINMAXINFO ourselves because chromium tries to
// get the scale factor of the window during it's version of this handler
// based on the window position, which is invalid at this point. The
// previous method of calling SetWindowPlacement fixed the window
// position for the scale factor calculation but broke other things.
MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(l_param);
// We do this to work around a Windows bug, where the minimized Window
// would report that the closest display to it is not the one that it was
// previously on (but the leftmost one instead). We restore the position
// of the window during the restore operation, this way chromium can
// use the proper display to calculate the scale factor to use.
if (!last_normal_placement_bounds_.IsEmpty() &&
GetWindowPlacement(GetAcceleratedWidget(), &wp)) {
last_normal_placement_bounds_.set_size(gfx::Size(0, 0));
wp.rcNormalPosition = last_normal_placement_bounds_.ToRECT();
SetWindowPlacement(GetAcceleratedWidget(), &wp);
display::Display display =
display::Screen::GetScreen()->GetDisplayNearestPoint(
last_normal_placement_bounds_.origin());
last_normal_placement_bounds_ = gfx::Rect();
gfx::Size min_size = gfx::ScaleToCeiledSize(
widget()->GetMinimumSize(), display.device_scale_factor());
gfx::Size max_size = gfx::ScaleToCeiledSize(
widget()->GetMaximumSize(), display.device_scale_factor());
info->ptMinTrackSize.x = min_size.width();
info->ptMinTrackSize.y = min_size.height();
if (max_size.width() || max_size.height()) {
if (!max_size.width())
max_size.set_width(GetSystemMetrics(SM_CXMAXTRACK));
if (!max_size.height())
max_size.set_height(GetSystemMetrics(SM_CYMAXTRACK));
info->ptMaxTrackSize.x = max_size.width();
info->ptMaxTrackSize.y = max_size.height();
}
return false;
*result = 1;
return true;
}
case WM_NCCALCSIZE: {
if (!has_frame() && w_param == TRUE) {
@@ -378,8 +389,21 @@ bool NativeWindowViews::PreHandleMSG(UINT message,
// https://blogs.msdn.microsoft.com/wpfsdk/2008/09/08/custom-window-chrome-in-wpf/
DefWindowProcW(GetAcceleratedWidget(), WM_NCCALCSIZE, w_param, l_param);
// When fullscreen the window has no border
int border = 0;
if (!IsFullscreen()) {
// When not fullscreen calculate the border size
border = GetSystemMetrics(SM_CXFRAME) +
GetSystemMetrics(SM_CXPADDEDBORDER);
if (!thick_frame_) {
border -= GetSystemMetrics(SM_CXBORDER);
}
}
if (last_window_state_ == ui::SHOW_STATE_MAXIMIZED) {
params->rgrc[0].top = 0;
// Position the top of the frame offset from where windows thinks by
// exactly the border amount. When fullscreen this is 0.
params->rgrc[0].top = PROPOSED.top + border;
} else {
params->rgrc[0] = PROPOSED;
params->rgrc[1] = BEFORE;

View File

@@ -396,12 +396,9 @@ void AtomURLLoaderFactory::StartLoadingHttp(
}
}
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
content::BrowserContext::GetDefaultStoragePartition(browser_context.get())
->GetURLLoaderFactoryForBrowserProcess();
new URLPipeLoader(
url_loader_factory, std::move(request), std::move(loader),
std::move(client),
browser_context->GetURLLoaderFactory(), std::move(request),
std::move(loader), std::move(client),
static_cast<net::NetworkTrafficAnnotationTag>(traffic_annotation),
std::move(upload_data));
}

View File

@@ -50,8 +50,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 8,0,0,20190827
PRODUCTVERSION 8,0,0,20190827
FILEVERSION 8,0,0,20190903
PRODUCTVERSION 8,0,0,20190903
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L

View File

@@ -89,8 +89,9 @@ void GenerateAcceleratorTable(AcceleratorTable* table,
bool TriggerAcceleratorTableCommand(AcceleratorTable* table,
const ui::Accelerator& accelerator) {
if (base::Contains(*table, accelerator)) {
const accelerator_util::MenuItem& item = (*table)[accelerator];
const auto iter = table->find(accelerator);
if (iter != std::end(*table)) {
const accelerator_util::MenuItem& item = iter->second;
if (item.model->IsEnabledAt(item.position)) {
const auto event_flags =
accelerator.MaskOutKeyEventFlags(accelerator.modifiers());

View File

@@ -25,11 +25,9 @@ void AtomMenuModel::SetToolTip(int index, const base::string16& toolTip) {
}
base::string16 AtomMenuModel::GetToolTipAt(int index) {
int command_id = GetCommandIdAt(index);
if (base::Contains(toolTips_, command_id))
return toolTips_[command_id];
else
return base::string16();
const int command_id = GetCommandIdAt(index);
const auto iter = toolTips_.find(command_id);
return iter == std::end(toolTips_) ? base::string16() : iter->second;
}
void AtomMenuModel::SetRole(int index, const base::string16& role) {
@@ -38,11 +36,9 @@ void AtomMenuModel::SetRole(int index, const base::string16& role) {
}
base::string16 AtomMenuModel::GetRoleAt(int index) {
int command_id = GetCommandIdAt(index);
if (base::Contains(roles_, command_id))
return roles_[command_id];
else
return base::string16();
const int command_id = GetCommandIdAt(index);
const auto iter = roles_.find(command_id);
return iter == std::end(roles_) ? base::string16() : iter->second;
}
bool AtomMenuModel::GetAcceleratorAtWithParams(

View File

@@ -270,12 +270,6 @@ void MenuBar::OnMenuButtonClicked(views::Button* source,
if (!window_->HasFocus())
window_->RequestFocus();
// This ensures that if you focus the menubar by clicking on an item, you can
// still use the arrow keys to move around
if (GetPaneFocusTraversable() == nullptr) {
SetPaneFocus(source);
}
int id = source->tag();
AtomMenuModel::ItemType type = menu_model_->GetTypeAt(id);
if (type != AtomMenuModel::TYPE_SUBMENU) {

View File

@@ -199,8 +199,9 @@ bool TaskbarHost::SetThumbnailToolTip(HWND window, const std::string& tooltip) {
}
bool TaskbarHost::HandleThumbarButtonEvent(int button_id) {
if (base::Contains(callback_map_, button_id)) {
auto callback = callback_map_[button_id];
const auto iter = callback_map_.find(button_id);
if (iter != std::end(callback_map_)) {
auto callback = iter->second;
if (!callback.is_null())
callback.Run();
return true;

View File

@@ -124,7 +124,6 @@ WebContentsPreferences::WebContentsPreferences(
SetDefaultBoolIfUndefined(options::kWebviewTag, false);
SetDefaultBoolIfUndefined(options::kSandbox, false);
SetDefaultBoolIfUndefined(options::kNativeWindowOpen, false);
SetDefaultBoolIfUndefined(options::kEnableRemoteModule, true);
SetDefaultBoolIfUndefined(options::kContextIsolation, false);
SetDefaultBoolIfUndefined(options::kJavaScript, true);
SetDefaultBoolIfUndefined(options::kImages, true);
@@ -171,6 +170,8 @@ WebContentsPreferences::~WebContentsPreferences() {
}
void WebContentsPreferences::SetDefaults() {
SetDefaultBoolIfUndefined(options::kEnableRemoteModule, true);
if (IsEnabled(options::kSandbox)) {
SetBool(options::kNativeWindowOpen, true);
}
@@ -220,10 +221,6 @@ bool WebContentsPreferences::GetPreference(base::StringPiece name,
return GetAsString(&preference_, name, value);
}
bool WebContentsPreferences::IsRemoteModuleEnabled() const {
return IsEnabled(options::kEnableRemoteModule, true);
}
bool WebContentsPreferences::GetPreloadPath(
base::FilePath::StringType* path) const {
DCHECK(path);
@@ -327,8 +324,8 @@ void WebContentsPreferences::AppendCommandLineSwitches(
}
// Whether to enable the remote module
if (!IsRemoteModuleEnabled())
command_line->AppendSwitch(switches::kDisableRemoteModule);
if (IsEnabled(options::kEnableRemoteModule))
command_line->AppendSwitch(switches::kEnableRemoteModule);
// Run Electron APIs and preload script in isolated world
if (IsEnabled(options::kContextIsolation))

View File

@@ -58,9 +58,6 @@ class WebContentsPreferences
// Return true if the particular preference value exists.
bool GetPreference(base::StringPiece name, std::string* value) const;
// Whether to enable the remote module
bool IsRemoteModuleEnabled() const;
// Returns the preload script path.
bool GetPreloadPath(base::FilePath::StringType* path) const;

View File

@@ -28,11 +28,9 @@ void WebViewManager::AddGuest(int guest_instance_id,
}
void WebViewManager::RemoveGuest(int guest_instance_id) {
if (!base::Contains(web_contents_embedder_map_, guest_instance_id))
if (web_contents_embedder_map_.erase(guest_instance_id) == 0)
return;
web_contents_embedder_map_.erase(guest_instance_id);
// Remove the record of element in embedder too.
for (const auto& element : element_instance_id_to_guest_map_)
if (element.second == guest_instance_id) {
@@ -42,24 +40,24 @@ void WebViewManager::RemoveGuest(int guest_instance_id) {
}
content::WebContents* WebViewManager::GetEmbedder(int guest_instance_id) {
if (base::Contains(web_contents_embedder_map_, guest_instance_id))
return web_contents_embedder_map_[guest_instance_id].embedder;
else
return nullptr;
const auto iter = web_contents_embedder_map_.find(guest_instance_id);
return iter == std::end(web_contents_embedder_map_) ? nullptr
: iter->second.embedder;
}
content::WebContents* WebViewManager::GetGuestByInstanceID(
int owner_process_id,
int element_instance_id) {
ElementInstanceKey key(owner_process_id, element_instance_id);
if (!base::Contains(element_instance_id_to_guest_map_, key))
const ElementInstanceKey key(owner_process_id, element_instance_id);
const auto guest_iter = element_instance_id_to_guest_map_.find(key);
if (guest_iter == std::end(element_instance_id_to_guest_map_))
return nullptr;
int guest_instance_id = element_instance_id_to_guest_map_[key];
if (base::Contains(web_contents_embedder_map_, guest_instance_id))
return web_contents_embedder_map_[guest_instance_id].web_contents;
else
return nullptr;
const int guest_instance_id = guest_iter->second;
const auto iter = web_contents_embedder_map_.find(guest_instance_id);
return iter == std::end(web_contents_embedder_map_)
? nullptr
: iter->second.web_contents;
}
bool WebViewManager::ForEachGuest(content::WebContents* embedder_web_contents,

View File

@@ -65,16 +65,27 @@ void RemoteObjectFreer::RunDestructor() {
if (!render_frame)
return;
// Reset our local ref count in case we are in a GC race condition
// and will get more references in an inbound IPC message
int ref_count = 0;
const auto objects_it = ref_mapper_.find(context_id_);
if (objects_it != std::end(ref_mapper_)) {
auto& objects = objects_it->second;
const auto ref_it = objects.find(object_id_);
if (ref_it != std::end(objects)) {
ref_count = ref_it->second;
objects.erase(ref_it);
}
if (objects.empty())
ref_mapper_.erase(objects_it);
}
auto* channel = "ELECTRON_BROWSER_DEREFERENCE";
base::ListValue args;
args.AppendString(context_id_);
args.AppendInteger(object_id_);
args.AppendInteger(ref_mapper_[context_id_][object_id_]);
// Reset our local ref count in case we are in a GC race condition and will
// get more references in an inbound IPC message
ref_mapper_[context_id_].erase(object_id_);
if (ref_mapper_[context_id_].empty())
ref_mapper_.erase(context_id_);
args.AppendInteger(ref_count);
mojom::ElectronBrowserAssociatedPtr electron_ptr;
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(

View File

@@ -30,14 +30,22 @@ const base::FilePath::CharType kAsarExtension[] = FILE_PATH_LITERAL(".asar");
std::shared_ptr<Archive> GetOrCreateAsarArchive(const base::FilePath& path) {
if (!g_archive_map_tls.Pointer()->Get())
g_archive_map_tls.Pointer()->Set(new ArchiveMap);
ArchiveMap& archive_map = *g_archive_map_tls.Pointer()->Get();
if (!base::Contains(archive_map, path)) {
std::shared_ptr<Archive> archive(new Archive(path));
if (!archive->Init())
return nullptr;
archive_map[path] = archive;
ArchiveMap& map = *g_archive_map_tls.Pointer()->Get();
// if we have it, return it
const auto lower = map.lower_bound(path);
if (lower != std::end(map) && !map.key_comp()(path, lower->first))
return lower->second;
// if we can create it, return it
auto archive = std::make_shared<Archive>(path);
if (archive->Init()) {
base::TryEmplace(map, lower, path, archive);
return archive;
}
return archive_map[path];
// didn't have it, couldn't create it
return nullptr;
}
void ClearArchives() {

View File

@@ -224,7 +224,7 @@ const char kBackgroundColor[] = "background-color";
const char kPreloadScript[] = "preload";
const char kPreloadScripts[] = "preload-scripts";
const char kNodeIntegration[] = "node-integration";
const char kDisableRemoteModule[] = "disable-remote-module";
const char kEnableRemoteModule[] = "enable-remote-module";
const char kContextIsolation[] = "context-isolation";
const char kGuestInstanceID[] = "guest-instance-id";
const char kOpenerID[] = "opener-id";

View File

@@ -107,7 +107,7 @@ extern const char kBackgroundColor[];
extern const char kPreloadScript[];
extern const char kPreloadScripts[];
extern const char kNodeIntegration[];
extern const char kDisableRemoteModule[];
extern const char kEnableRemoteModule[];
extern const char kContextIsolation[];
extern const char kGuestInstanceID[];
extern const char kOpenerID[];

View File

@@ -153,14 +153,12 @@ void AtomRendererClient::DidCreateScriptContext(
void AtomRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) {
if (injected_frames_.find(render_frame) == injected_frames_.end())
if (injected_frames_.erase(render_frame) == 0)
return;
injected_frames_.erase(render_frame);
node::Environment* env = node::Environment::GetCurrent(context);
if (environments_.find(env) == environments_.end())
if (environments_.erase(env) == 0)
return;
environments_.erase(env);
mate::EmitEvent(env->isolate(), env->process_object(), "exit");

View File

@@ -294,9 +294,8 @@ void AtomSandboxedRendererClient::SetupExtensionWorldOverrides(
void AtomSandboxedRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) {
if (injected_frames_.find(render_frame) == injected_frames_.end())
if (injected_frames_.erase(render_frame) == 0)
return;
injected_frames_.erase(render_frame);
auto* isolate = context->GetIsolate();
v8::HandleScope handle_scope(isolate);

View File

@@ -119,7 +119,7 @@ void RendererClientBase::DidCreateScriptContext(
auto* command_line = base::CommandLine::ForCurrentProcess();
bool enableRemoteModule =
!command_line->HasSwitch(switches::kDisableRemoteModule);
command_line->HasSwitch(switches::kEnableRemoteModule);
global.SetHidden("enableRemoteModule", enableRemoteModule);
}

View File

@@ -1,4 +1,5 @@
declare var isCI: boolean;
declare var standardScheme: string;
declare namespace Electron {
interface Menu {
@@ -15,6 +16,11 @@ declare namespace Electron {
interface WebContents {
getOwnerBrowserWindow(): BrowserWindow;
getWebPreferences(): any;
}
interface Session {
destroy(): void;
}
// Experimental views API
@@ -23,4 +29,9 @@ declare namespace Electron {
setContentView(view: View): void
}
class View {}
class WebContentsView {
constructor(webContents: WebContents)
}
}
declare module 'dbus-native';

View File

@@ -1,19 +1,16 @@
'use strict'
import { expect } from 'chai'
import * as path from 'path'
const { expect } = require('chai')
const path = require('path')
const { remote } = require('electron')
const { ipcMain, BrowserWindow } = remote
const { closeWindow } = require('./window-helpers')
import { ipcMain, BrowserWindow, WebPreferences } from 'electron'
import { closeWindow } from './window-helpers'
describe('BrowserWindow with affinity module', () => {
const fixtures = path.resolve(__dirname, 'fixtures')
const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures')
const myAffinityName = 'myAffinity'
const myAffinityNameUpper = 'MYAFFINITY'
const anotherAffinityName = 'anotherAffinity'
async function createWindowWithWebPrefs (webPrefs) {
async function createWindowWithWebPrefs (webPrefs: WebPreferences) {
const w = new BrowserWindow({
show: false,
width: 400,
@@ -24,16 +21,16 @@ describe('BrowserWindow with affinity module', () => {
return w
}
function testAffinityProcessIds (name, webPreferences = {}) {
function testAffinityProcessIds (name: string, webPreferences: WebPreferences = {}) {
describe(name, () => {
let mAffinityWindow
let mAffinityWindow: BrowserWindow
before(async () => {
mAffinityWindow = await createWindowWithWebPrefs({ affinity: myAffinityName, ...webPreferences })
})
after(async () => {
await closeWindow(mAffinityWindow, { assertSingleWindow: false })
mAffinityWindow = null
await closeWindow(mAffinityWindow, { assertNotWindows: false })
mAffinityWindow = null as unknown as BrowserWindow
})
it('should have a different process id than a default window', async () => {
@@ -42,7 +39,7 @@ describe('BrowserWindow with affinity module', () => {
const wcID = w.webContents.getOSProcessId()
expect(affinityID).to.not.equal(wcID, 'Should have different OS process IDs')
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
it(`should have a different process id than a window with a different affinity '${anotherAffinityName}'`, async () => {
@@ -51,7 +48,7 @@ describe('BrowserWindow with affinity module', () => {
const wcID = w.webContents.getOSProcessId()
expect(affinityID).to.not.equal(wcID, 'Should have different OS process IDs')
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
it(`should have the same OS process id than a window with the same affinity '${myAffinityName}'`, async () => {
@@ -60,7 +57,7 @@ describe('BrowserWindow with affinity module', () => {
const wcID = w.webContents.getOSProcessId()
expect(affinityID).to.equal(wcID, 'Should have the same OS process ID')
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
it(`should have the same OS process id than a window with an equivalent affinity '${myAffinityNameUpper}' (case insensitive)`, async () => {
@@ -69,7 +66,7 @@ describe('BrowserWindow with affinity module', () => {
const wcID = w.webContents.getOSProcessId()
expect(affinityID).to.equal(wcID, 'Should have the same OS process ID')
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
})
}
@@ -83,7 +80,7 @@ describe('BrowserWindow with affinity module', () => {
const affinityWithNodeTrue = 'affinityWithNodeTrue'
const affinityWithNodeFalse = 'affinityWithNodeFalse'
function testNodeIntegration (present) {
function testNodeIntegration (present: boolean) {
return new Promise((resolve, reject) => {
ipcMain.once('answer', (event, typeofProcess, typeofBuffer) => {
if (present) {
@@ -107,7 +104,7 @@ describe('BrowserWindow with affinity module', () => {
nodeIntegration: false
})
])
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
it('disables node integration when first window is false', async () => {
const [, w1] = await Promise.all([
@@ -127,8 +124,8 @@ describe('BrowserWindow with affinity module', () => {
})
])
await Promise.all([
closeWindow(w1, { assertSingleWindow: false }),
closeWindow(w2, { assertSingleWindow: false })
closeWindow(w1, { assertNotWindows: false }),
closeWindow(w2, { assertNotWindows: false })
])
})
@@ -141,7 +138,7 @@ describe('BrowserWindow with affinity module', () => {
nodeIntegration: true
})
])
await closeWindow(w, { assertSingleWindow: false })
await closeWindow(w, { assertNotWindows: false })
})
it('enables node integration when first window is true', async () => {
@@ -162,8 +159,8 @@ describe('BrowserWindow with affinity module', () => {
})
])
await Promise.all([
closeWindow(w1, { assertSingleWindow: false }),
closeWindow(w2, { assertSingleWindow: false })
closeWindow(w1, { assertNotWindows: false }),
closeWindow(w2, { assertNotWindows: false })
])
})
})

View File

@@ -1,53 +1,48 @@
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const chaiAsPromised = require('chai-as-promised')
const { desktopCapturer, ipcRenderer, remote } = require('electron')
const { screen } = remote
import { expect } from 'chai'
import { desktopCapturer, ipcRenderer, screen, BrowserWindow, SourcesOptions } from 'electron'
import { emittedOnce } from './events-helpers'
import { ifdescribe } from './spec-helpers';
import { closeAllWindows } from './window-helpers';
const features = process.electronBinding('features')
const { emittedOnce } = require('./events-helpers')
const { expect } = chai
chai.use(dirtyChai)
chai.use(chaiAsPromised)
const isCI = remote.getGlobal('isCi')
describe('desktopCapturer', () => {
before(function () {
if (!features.isDesktopCapturerEnabled() || process.arch.indexOf('arm') === 0) {
// It's been disabled during build time.
this.skip()
return
}
if (isCI && process.platform === 'win32') {
this.skip()
}
ifdescribe(features.isDesktopCapturerEnabled() && !process.arch.includes('arm') && process.platform !== 'win32')('desktopCapturer', () => {
let w: BrowserWindow
before(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadURL('about:blank')
})
after(closeAllWindows)
const getSources: typeof desktopCapturer.getSources = (options: SourcesOptions) => {
return w.webContents.executeJavaScript(`
require('electron').desktopCapturer.getSources(${JSON.stringify(options)})
`)
}
it('should return a non-empty array of sources', async () => {
const sources = await desktopCapturer.getSources({ types: ['window', 'screen'] })
const sources = await getSources({ types: ['window', 'screen'] })
expect(sources).to.be.an('array').that.is.not.empty()
})
it('throws an error for invalid options', async () => {
const promise = desktopCapturer.getSources(['window', 'screen'])
const promise = getSources(['window', 'screen'] as any)
expect(promise).to.be.eventually.rejectedWith(Error, 'Invalid options')
})
it('does not throw an error when called more than once (regression)', async () => {
const sources1 = await desktopCapturer.getSources({ types: ['window', 'screen'] })
const sources1 = await getSources({ types: ['window', 'screen'] })
expect(sources1).to.be.an('array').that.is.not.empty()
const sources2 = await desktopCapturer.getSources({ types: ['window', 'screen'] })
const sources2 = await getSources({ types: ['window', 'screen'] })
expect(sources2).to.be.an('array').that.is.not.empty()
})
it('responds to subsequent calls of different options', async () => {
const promise1 = desktopCapturer.getSources({ types: ['window'] })
const promise1 = getSources({ types: ['window'] })
expect(promise1).to.not.eventually.be.rejected()
const promise2 = desktopCapturer.getSources({ types: ['screen'] })
const promise2 = getSources({ types: ['screen'] })
expect(promise2).to.not.eventually.be.rejected()
})
@@ -55,10 +50,9 @@ describe('desktopCapturer', () => {
// Linux doesn't return any window sources.
if (process.platform !== 'win32' && process.platform !== 'darwin') return
const { BrowserWindow } = remote
const w = new BrowserWindow({ width: 200, height: 200 })
const sources = await desktopCapturer.getSources({ types: ['window'] })
const sources = await getSources({ types: ['window'] })
w.destroy()
expect(sources).to.be.an('array').that.is.not.empty()
for (const { display_id: displayId } of sources) {
@@ -70,7 +64,7 @@ describe('desktopCapturer', () => {
if (process.platform !== 'win32' && process.platform !== 'darwin') return
const displays = screen.getAllDisplays()
const sources = await desktopCapturer.getSources({ types: ['screen'] })
const sources = await getSources({ types: ['screen'] })
expect(sources).to.be.an('array').of.length(displays.length)
for (let i = 0; i < sources.length; i++) {
@@ -79,32 +73,32 @@ describe('desktopCapturer', () => {
it('returns empty sources when blocked', async () => {
ipcRenderer.send('handle-next-desktop-capturer-get-sources')
const sources = await desktopCapturer.getSources({ types: ['screen'] })
const sources = await getSources({ types: ['screen'] })
expect(sources).to.be.empty()
})
})
it('disabling thumbnail should return empty images', async () => {
const { BrowserWindow } = remote
const w = new BrowserWindow({ show: false, width: 200, height: 200 })
const wShown = emittedOnce(w, 'show')
w.show()
const w2 = new BrowserWindow({ show: false, width: 200, height: 200 })
const wShown = emittedOnce(w2, 'show')
w2.show()
await wShown
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 0, height: 0 }
})
w.destroy()
expect(sources).to.be.an('array').that.is.not.empty()
for (const { thumbnail: thumbnailImage } of sources) {
expect(thumbnailImage).to.be.a('NativeImage')
expect(thumbnailImage.isEmpty()).to.be.true()
}
const isEmpties: boolean[] = await w.webContents.executeJavaScript(`
require('electron').desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 0, height: 0 }
}).then((sources) => {
return sources.map(s => s.thumbnail.constructor.name === 'NativeImage' && s.thumbnail.isEmpty())
})
`)
w2.destroy()
expect(isEmpties).to.be.an('array').that.is.not.empty()
expect(isEmpties.every(e => e === true)).to.be.true()
})
it('getMediaSourceId should match DesktopCapturerSource.id', async () => {
const { BrowserWindow } = remote
const w = new BrowserWindow({ show: false, width: 100, height: 100 })
const wShown = emittedOnce(w, 'show')
const wFocused = emittedOnce(w, 'focus')
@@ -114,7 +108,7 @@ describe('desktopCapturer', () => {
await wFocused
const mediaSourceId = w.getMediaSourceId()
const sources = await desktopCapturer.getSources({
const sources = await getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
})
@@ -132,18 +126,16 @@ describe('desktopCapturer', () => {
const foundSource = sources.find((source) => {
return source.id === mediaSourceId
})
expect(mediaSourceId).to.equal(foundSource.id)
expect(mediaSourceId).to.equal(foundSource!.id)
})
it('moveAbove should move the window at the requested place', async () => {
// DesktopCapturer.getSources() is guaranteed to return in the correct
// z-order from foreground to background.
const MAX_WIN = 4
const { BrowserWindow } = remote
const mainWindow = remote.getCurrentWindow()
const mainWindow = w
const wList = [mainWindow]
try {
// Add MAX_WIN-1 more window so we have MAX_WIN in total.
for (let i = 0; i < MAX_WIN - 1; i++) {
const w = new BrowserWindow({ show: true, width: 100, height: 100 })
wList.push(w)
@@ -160,7 +152,7 @@ describe('desktopCapturer', () => {
// DesktopCapturer.getSources() returns sources sorted from foreground to
// background, i.e. top to bottom.
let sources = await desktopCapturer.getSources({
let sources = await getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
})
@@ -206,7 +198,7 @@ describe('desktopCapturer', () => {
}
})
sources = await desktopCapturer.getSources({
sources = await getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
})

View File

@@ -628,7 +628,7 @@ describe('net module', () => {
})
})
describe.skip('webRequest', () => {
describe('webRequest', () => {
afterEach(() => {
session.defaultSession.webRequest.onBeforeRequest(null)
})

View File

@@ -6,20 +6,19 @@
//
// See https://pypi.python.org/pypi/python-dbusmock to read about dbusmock.
const { expect } = require('chai')
const dbus = require('dbus-native')
const Promise = require('bluebird')
const { remote } = require('electron')
const { app } = remote
import { expect } from 'chai'
import * as dbus from 'dbus-native'
import { app } from 'electron'
import { ifdescribe } from './spec-helpers'
import { promisify } from 'util';
const skip = process.platform !== 'linux' ||
process.arch === 'ia32' ||
process.arch.indexOf('arm') === 0 ||
!process.env.DBUS_SESSION_BUS_ADDRESS;
!process.env.DBUS_SESSION_BUS_ADDRESS
(skip ? describe.skip : describe)('Notification module (dbus)', () => {
let mock, Notification, getCalls, reset
ifdescribe(!skip)('Notification module (dbus)', () => {
let mock: any, Notification, getCalls: any, reset: any
const realAppName = app.name
const realAppVersion = app.getVersion()
const appName = 'api-notification-dbus-spec'
@@ -35,10 +34,10 @@ const skip = process.platform !== 'linux' ||
const bus = dbus.sessionBus()
console.log(`session bus: ${process.env.DBUS_SESSION_BUS_ADDRESS}`)
const service = bus.getService(serviceName)
const getInterface = Promise.promisify(service.getInterface, { context: service })
const getInterface = promisify(service.getInterface.bind(service))
mock = await getInterface(path, iface)
getCalls = Promise.promisify(mock.GetCalls, { context: mock })
reset = Promise.promisify(mock.Reset, { context: mock })
getCalls = promisify(mock.GetCalls.bind(mock))
reset = promisify(mock.Reset.bind(mock))
})
after(async () => {
@@ -50,8 +49,8 @@ const skip = process.platform !== 'linux' ||
})
describe(`Notification module using ${serviceName}`, () => {
function onMethodCalled (done) {
function cb (name) {
function onMethodCalled (done: () => void) {
function cb (name: string) {
console.log(`onMethodCalled: ${name}`)
if (name === 'Notify') {
mock.removeListener('MethodCalled', cb)
@@ -62,8 +61,8 @@ const skip = process.platform !== 'linux' ||
return cb
}
function unmarshalDBusNotifyHints (dbusHints) {
const o = {}
function unmarshalDBusNotifyHints (dbusHints: any) {
const o: Record<string, any> = {}
for (const hint of dbusHints) {
const key = hint[0]
const value = hint[1][1][0]
@@ -72,7 +71,7 @@ const skip = process.platform !== 'linux' ||
return o
}
function unmarshalDBusNotifyArgs (dbusArgs) {
function unmarshalDBusNotifyArgs (dbusArgs: any) {
return {
app_name: dbusArgs[0][1][0],
replaces_id: dbusArgs[1][1][0],
@@ -87,7 +86,7 @@ const skip = process.platform !== 'linux' ||
before(done => {
mock.on('MethodCalled', onMethodCalled(done))
// lazy load Notification after we listen to MethodCalled mock signal
Notification = require('electron').remote.Notification
Notification = require('electron').Notification
const n = new Notification({
title: 'title',
subtitle: 'subtitle',

View File

@@ -6,37 +6,30 @@
//
// See https://pypi.python.org/pypi/python-dbusmock for more information about
// python-dbusmock.
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const dbus = require('dbus-native')
const Promise = require('bluebird')
const { expect } = chai
chai.use(dirtyChai)
const skip = process.platform !== 'linux' || !process.env.DBUS_SYSTEM_BUS_ADDRESS
import { expect } from 'chai'
import * as dbus from 'dbus-native'
import { ifdescribe } from './spec-helpers'
import { promisify } from 'util'
describe('powerMonitor', () => {
let logindMock, dbusMockPowerMonitor, getCalls, emitSignal, reset
let logindMock: any, dbusMockPowerMonitor: any, getCalls: any, emitSignal: any, reset: any
if (!skip) {
ifdescribe(process.platform === 'linux' && process.env.DBUS_SYSTEM_BUS_ADDRESS != null)('when powerMonitor module is loaded with dbus mock', () => {
before(async () => {
const systemBus = dbus.systemBus()
const loginService = systemBus.getService('org.freedesktop.login1')
const getInterface = Promise.promisify(loginService.getInterface, { context: loginService })
const getInterface = promisify(loginService.getInterface.bind(loginService))
logindMock = await getInterface('/org/freedesktop/login1', 'org.freedesktop.DBus.Mock')
getCalls = Promise.promisify(logindMock.GetCalls, { context: logindMock })
emitSignal = Promise.promisify(logindMock.EmitSignal, { context: logindMock })
reset = Promise.promisify(logindMock.Reset, { context: logindMock })
getCalls = promisify(logindMock.GetCalls.bind(logindMock))
emitSignal = promisify(logindMock.EmitSignal.bind(logindMock))
reset = promisify(logindMock.Reset.bind(logindMock))
})
after(async () => {
await reset()
})
}
(skip ? describe.skip : describe)('when powerMonitor module is loaded with dbus mock', () => {
function onceMethodCalled (done) {
function onceMethodCalled (done: () => void) {
function cb () {
logindMock.removeListener('MethodCalled', cb)
}
@@ -47,7 +40,7 @@ describe('powerMonitor', () => {
before(done => {
logindMock.on('MethodCalled', onceMethodCalled(done))
// lazy load powerMonitor after we listen to MethodCalled mock signal
dbusMockPowerMonitor = require('electron').remote.powerMonitor
dbusMockPowerMonitor = require('electron').powerMonitor
})
it('should call Inhibit to delay suspend', async () => {
@@ -123,11 +116,10 @@ describe('powerMonitor', () => {
})
describe('when powerMonitor module is loaded', () => {
let powerMonitor
let powerMonitor: typeof Electron.powerMonitor
before(() => {
powerMonitor = require('electron').remote.powerMonitor
powerMonitor = require('electron').powerMonitor
})
describe('powerMonitor.getSystemIdleState', () => {
it('gets current system idle state', () => {
// this function is not mocked out, so we can test the result's
@@ -148,7 +140,7 @@ describe('powerMonitor', () => {
}).to.throw(/conversion failure/)
expect(() => {
powerMonitor.getSystemIdleState('a')
powerMonitor.getSystemIdleState('a' as any)
}).to.throw(/conversion failure/)
})
})

View File

@@ -1,41 +1,23 @@
const chai = require('chai')
const http = require('http')
const https = require('https')
const path = require('path')
const fs = require('fs')
const send = require('send')
const auth = require('basic-auth')
const ChildProcess = require('child_process')
const { closeWindow } = require('./window-helpers')
const { emittedOnce } = require('./events-helpers')
const { session, BrowserWindow, net, ipcMain } = require('electron')
const { expect } = chai
import { expect } from 'chai'
import * as http from 'http'
import * as https from 'https'
import * as path from 'path'
import * as fs from 'fs'
import * as ChildProcess from 'child_process'
import { session, BrowserWindow, net, ipcMain, Session } from 'electron'
import * as send from 'send'
import * as auth from 'basic-auth'
import { closeAllWindows } from './window-helpers'
import { emittedOnce } from './events-helpers'
import { AddressInfo } from 'net';
/* The whole session API doesn't use standard callbacks */
/* eslint-disable standard/no-callback-literal */
describe('session module', () => {
const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures')
let w = null
const url = 'http://127.0.0.1'
beforeEach(() => {
w = new BrowserWindow({
show: false,
width: 400,
height: 400,
webPreferences: {
nodeIntegration: true,
webviewTag: true,
}
})
})
afterEach(() => {
return closeWindow(w).then(() => { w = null })
})
describe('session.defaultSession', () => {
it('returns the default session', () => {
expect(session.defaultSession).to.equal(session.fromPartition(''))
@@ -59,21 +41,22 @@ describe('session module', () => {
expect(ses2.getUserAgent()).to.not.equal(userAgent)
})
it('created session is ref-counted', () => {
it.skip('created session is ref-counted', () => {
const partition = 'test2'
const userAgent = 'test-agent'
const ses1 = session.fromPartition(partition)
ses1.userAgent = userAgent
expect(ses1.userAgent).to.equal(userAgent)
ses1.setUserAgent(userAgent)
expect(ses1.getUserAgent()).to.equal(userAgent)
ses1.destroy()
const ses2 = session.fromPartition(partition)
expect(ses2.userAgent).to.not.equal(userAgent)
expect(ses2.getUserAgent()).to.not.equal(userAgent)
})
})
describe('ses.cookies', () => {
const name = '0'
const value = '0'
afterEach(closeAllWindows)
it('should get cookies', async () => {
const server = http.createServer((req, res) => {
@@ -82,7 +65,8 @@ describe('session module', () => {
server.close()
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
const { port } = server.address() as AddressInfo
const w = new BrowserWindow({ show: false })
await w.loadURL(`${url}:${port}`)
const list = await w.webContents.session.cookies.get({ url })
const cookie = list.find(cookie => cookie.name === name)
@@ -144,7 +128,6 @@ describe('session module', () => {
it.skip('should set cookie for standard scheme', async () => {
const { cookies } = session.defaultSession
const standardScheme = global.standardScheme
const domain = 'fake-host'
const url = `${standardScheme}://${domain}`
const name = 'custom'
@@ -160,12 +143,9 @@ describe('session module', () => {
})
it('emits a changed event when a cookie is added or removed', async () => {
const changes = []
const { cookies } = session.fromPartition('cookies-changed')
const name = 'foo'
const value = 'bar'
const listener = (event, cookie, cause, removed) => { changes.push({ cookie, cause, removed }) }
const a = emittedOnce(cookies, 'changed')
await cookies.set({ url, name, value, expirationDate: (+new Date()) / 1000 + 120 })
@@ -200,7 +180,7 @@ describe('session module', () => {
it('should survive an app restart for persistent partition', async () => {
const appPath = path.join(fixtures, 'api', 'cookie-app')
const runAppWithPhase = (phase) => {
const runAppWithPhase = (phase: string) => {
return new Promise((resolve, reject) => {
let output = ''
@@ -223,7 +203,9 @@ describe('session module', () => {
})
describe('ses.clearStorageData(options)', () => {
afterEach(closeAllWindows)
it('clears localstorage data', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadFile(path.join(fixtures, 'api', 'localstorage.html'))
const options = {
origin: 'file://',
@@ -239,7 +221,9 @@ describe('session module', () => {
})
describe('will-download event', () => {
afterEach(closeAllWindows)
it('can cancel default download behavior', async () => {
const w = new BrowserWindow({ show: false })
const mockFile = Buffer.alloc(1024)
const contentDisposition = 'inline; filename="mockFile.txt"'
const downloadServer = http.createServer((req, res) => {
@@ -253,10 +237,10 @@ describe('session module', () => {
})
await new Promise(resolve => downloadServer.listen(0, '127.0.0.1', resolve))
const port = downloadServer.address().port
const port = (downloadServer.address() as AddressInfo).port
const url = `http://127.0.0.1:${port}/`
const downloadPrevented = new Promise(resolve => {
const downloadPrevented: Promise<Electron.DownloadItem> = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
e.preventDefault()
resolve(item)
@@ -274,29 +258,22 @@ describe('session module', () => {
describe('ses.protocol', () => {
const partitionName = 'temp'
const protocolName = 'sp'
let customSession = null
let customSession: Session
const protocol = session.defaultSession.protocol
const handler = (ignoredError, callback) => {
const handler = (ignoredError: any, callback: Function) => {
callback({ data: `<script>require('electron').ipcRenderer.send('hello')</script>`, mimeType: 'text/html' })
}
beforeEach(async () => {
if (w != null) w.destroy()
w = new BrowserWindow({
show: false,
webPreferences: {
partition: partitionName,
nodeIntegration: true,
}
})
customSession = session.fromPartition(partitionName)
await customSession.protocol.registerStringProtocol(protocolName, handler)
})
afterEach(async () => {
await customSession.protocol.unregisterProtocol(protocolName)
customSession = null
customSession = null as any
})
afterEach(closeAllWindows)
it('does not affect defaultSession', async () => {
const result1 = await protocol.isProtocolHandled(protocolName)
@@ -307,14 +284,23 @@ describe('session module', () => {
})
it('handles requests from partition', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
partition: partitionName,
nodeIntegration: true,
}
})
customSession = session.fromPartition(partitionName)
await customSession.protocol.registerStringProtocol(protocolName, handler)
w.loadURL(`${protocolName}://fake-host`)
await emittedOnce(ipcMain, 'hello')
})
})
describe('ses.setProxy(options)', () => {
let server = null
let customSession = null
let server: http.Server
let customSession: Electron.Session
beforeEach(async () => {
customSession = session.fromPartition('proxyconfig')
@@ -365,7 +351,7 @@ describe('session module', () => {
res.end(pac)
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const config = { pacScript: `http://127.0.0.1:${server.address().port}` }
const config = { pacScript: `http://127.0.0.1:${(server.address() as AddressInfo).port}` }
await customSession.setProxy(config)
const proxy = await customSession.resolveProxy('https://google.com')
expect(proxy).to.equal('PROXY myproxy:8132')
@@ -389,6 +375,7 @@ describe('session module', () => {
after(async () => {
await protocol.unregisterProtocol(scheme)
})
afterEach(closeAllWindows)
it('returns blob data for uuid', (done) => {
const postData = JSON.stringify({
@@ -409,20 +396,21 @@ describe('session module', () => {
} else if (request.method === 'POST') {
const uuid = request.uploadData[1].blobUUID
expect(uuid).to.be.a('string')
session.defaultSession.getBlobData(uuid).then(result => {
session.defaultSession.getBlobData(uuid!).then(result => {
expect(result.toString()).to.equal(postData)
done()
})
}
}, (error) => {
if (error) return done(error)
const w = new BrowserWindow({ show: false })
w.loadURL(url)
})
})
})
describe('ses.setCertificateVerifyProc(callback)', (done) => {
let server = null
describe('ses.setCertificateVerifyProc(callback)', () => {
let server: http.Server
beforeEach((done) => {
const certPath = path.join(fixtures, 'certificates')
@@ -448,6 +436,7 @@ describe('session module', () => {
session.defaultSession.setCertificateVerifyProc(null)
server.close(done)
})
afterEach(closeAllWindows)
it('accepts the request when the callback is called with 0', async () => {
session.defaultSession.setCertificateVerifyProc(({ hostname, certificate, verificationResult, errorCode }, callback) => {
@@ -456,7 +445,8 @@ describe('session module', () => {
callback(0)
})
await w.loadURL(`https://127.0.0.1:${server.address().port}`)
const w = new BrowserWindow({ show: false })
await w.loadURL(`https://127.0.0.1:${(server.address() as AddressInfo).port}`)
expect(w.webContents.getTitle()).to.equal('hello')
})
@@ -476,7 +466,8 @@ describe('session module', () => {
callback(-2)
})
const url = `https://127.0.0.1:${server.address().port}`
const url = `https://127.0.0.1:${(server.address() as AddressInfo).port}`
const w = new BrowserWindow({ show: false })
await expect(w.loadURL(url)).to.eventually.be.rejectedWith(/ERR_FAILED/)
expect(w.webContents.getTitle()).to.equal(url)
})
@@ -488,7 +479,8 @@ describe('session module', () => {
callback(-2)
})
const url = `https://127.0.0.1:${server.address().port}`
const url = `https://127.0.0.1:${(server.address() as AddressInfo).port}`
const w = new BrowserWindow({ show: false })
await expect(w.loadURL(url), 'first load').to.eventually.be.rejectedWith(/ERR_FAILED/)
await emittedOnce(w.webContents, 'did-stop-loading')
await expect(w.loadURL(url + '/test'), 'second load').to.eventually.be.rejectedWith(/ERR_FAILED/)
@@ -497,8 +489,8 @@ describe('session module', () => {
})
})
describe.skip('ses.clearAuthCache(options)', () => {
it('can clear http auth info from cache', (done) => {
describe('ses.clearAuthCache(options)', () => {
it('can clear http auth info from cache', async () => {
const ses = session.fromPartition('auth-cache')
const server = http.createServer((req, res) => {
const credentials = auth(req)
@@ -510,44 +502,31 @@ describe('session module', () => {
res.end('authenticated')
}
})
server.listen(0, '127.0.0.1', () => {
const port = server.address().port
function issueLoginRequest (attempt = 1) {
if (attempt > 2) {
server.close()
return done()
}
const request = net.request({
url: `http://127.0.0.1:${port}`,
session: ses
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const port = (server.address() as AddressInfo).port
const fetch = (url: string) => new Promise((resolve, reject) => {
const request = net.request({ url, session: ses })
request.on('response', (response) => {
let data = ''
response.on('data', (chunk) => {
data += chunk
})
request.on('login', (info, callback) => {
attempt += 1
expect(info.scheme).to.equal('basic')
expect(info.realm).to.equal('Restricted')
callback('test', 'test')
response.on('end', () => {
resolve(data)
})
request.on('response', (response) => {
let data = ''
response.pause()
response.on('data', (chunk) => {
data += chunk
})
response.on('end', () => {
expect(data).to.equal('authenticated')
ses.clearAuthCache({ type: 'password' }).then(() => {
issueLoginRequest(attempt)
})
})
response.on('error', (error) => { done(error) })
response.resume()
})
// Internal api to bypass cache for testing.
request.urlRequest._setLoadFlags(1 << 1)
request.end()
}
issueLoginRequest()
response.on('error', (error: any) => { reject(new Error(error)) })
});
request.end()
})
// the first time should throw due to unauthenticated
await expect(fetch(`http://127.0.0.1:${port}`)).to.eventually.be.rejected()
// passing the password should let us in
expect(await fetch(`http://test:test@127.0.0.1:${port}`)).to.equal('authenticated')
// subsequently, the credentials are cached
expect(await fetch(`http://127.0.0.1:${port}`)).to.equal('authenticated')
await ses.clearAuthCache({ type: 'password' })
// once the cache is cleared, we should get an error again
await expect(fetch(`http://127.0.0.1:${port}`)).to.eventually.be.rejected()
})
})
@@ -556,11 +535,10 @@ describe('session module', () => {
const downloadFilePath = path.join(__dirname, '..', 'fixtures', 'mock.pdf')
const protocolName = 'custom-dl'
const contentDisposition = 'inline; filename="mock.pdf"'
let address = null
let downloadServer = null
let address: AddressInfo
let downloadServer: http.Server
before(async () => {
downloadServer = http.createServer((req, res) => {
address = downloadServer.address()
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
@@ -569,15 +547,17 @@ describe('session module', () => {
res.end(mockPDF)
})
await new Promise(resolve => downloadServer.listen(0, '127.0.0.1', resolve))
address = downloadServer.address() as AddressInfo
})
after(async () => {
await new Promise(resolve => downloadServer.close(resolve))
})
afterEach(closeAllWindows)
const isPathEqual = (path1, path2) => {
const isPathEqual = (path1: string, path2: string) => {
return path.relative(path1, path2) === ''
}
const assertDownload = (state, item, isCustom = false) => {
const assertDownload = (state: string, item: Electron.DownloadItem, isCustom = false) => {
expect(state).to.equal('completed')
expect(item.getFilename()).to.equal('mock.pdf')
expect(path.isAbsolute(item.savePath)).to.equal(true)
@@ -595,8 +575,21 @@ describe('session module', () => {
fs.unlinkSync(downloadFilePath)
}
it('can download using session.downloadURL', (done) => {
const port = address.port
session.defaultSession.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
assertDownload(state, item)
done()
})
})
session.defaultSession.downloadURL(`${url}:${port}`)
})
it('can download using WebContents.downloadURL', (done) => {
const port = downloadServer.address().port
const port = address.port
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
@@ -609,12 +602,13 @@ describe('session module', () => {
it('can download from custom protocols using WebContents.downloadURL', (done) => {
const protocol = session.defaultSession.protocol
const port = downloadServer.address().port
const handler = (ignoredError, callback) => {
const port = address.port
const handler = (ignoredError: any, callback: Function) => {
callback({ url: `${url}:${port}` })
}
protocol.registerHttpProtocol(protocolName, handler, (error) => {
if (error) return done(error)
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
@@ -627,17 +621,18 @@ describe('session module', () => {
})
it('can download using WebView.downloadURL', async () => {
const port = downloadServer.address().port
const port = address.port
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } })
await w.loadURL('about:blank')
function webviewDownload({fixtures, url, port}) {
const webview = new WebView()
function webviewDownload({fixtures, url, port}: {fixtures: string, url: string, port: string}) {
const webview = new (window as any).WebView()
webview.addEventListener('did-finish-load', () => {
webview.downloadURL(`${url}:${port}/`)
})
webview.src = `file://${fixtures}/api/blank.html`
document.body.appendChild(webview)
}
const done = new Promise(resolve => {
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
@@ -651,7 +646,8 @@ describe('session module', () => {
})
it('can cancel download', (done) => {
const port = downloadServer.address().port
const port = address.port
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
@@ -675,7 +671,8 @@ describe('session module', () => {
return done()
}
const port = downloadServer.address().port
const port = address.port
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
item.on('done', function (e, state) {
@@ -689,7 +686,7 @@ describe('session module', () => {
it('can set options for the save dialog', (done) => {
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf')
const port = downloadServer.address().port
const port = address.port
const options = {
window: null,
title: 'title',
@@ -706,6 +703,7 @@ describe('session module', () => {
securityScopedBookmarks: true
}
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(filePath)
item.setSaveDialogOptions(options)
@@ -720,6 +718,7 @@ describe('session module', () => {
describe('when a save path is specified and the URL is unavailable', () => {
it('does not display a save dialog and reports the done state as interrupted', (done) => {
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath
if (item.getState() === 'interrupted') {
@@ -736,6 +735,7 @@ describe('session module', () => {
})
describe('ses.createInterruptedDownload(options)', () => {
afterEach(closeAllWindows)
it('can create an interrupted download item', (done) => {
const downloadFilePath = path.join(__dirname, '..', 'fixtures', 'mock.pdf')
const options = {
@@ -745,6 +745,7 @@ describe('session module', () => {
offset: 0,
length: 5242880
}
const w = new BrowserWindow({ show: false })
w.webContents.session.once('will-download', function (e, item) {
expect(item.getState()).to.equal('interrupted')
item.cancel()
@@ -762,13 +763,14 @@ describe('session module', () => {
const downloadFilePath = path.join(fixtures, 'logo.png')
const rangeServer = http.createServer((req, res) => {
const options = { root: fixtures }
send(req, req.url, options)
.on('error', (error) => { done(error) }).pipe(res)
send(req, req.url!, options)
.on('error', (error: any) => { throw error }).pipe(res)
})
try {
await new Promise(resolve => rangeServer.listen(0, '127.0.0.1', resolve))
const port = rangeServer.address().port
const downloadCancelled = new Promise((resolve) => {
const port = (rangeServer.address() as AddressInfo).port
const w = new BrowserWindow({ show: false })
const downloadCancelled: Promise<Electron.DownloadItem> = new Promise((resolve) => {
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
@@ -791,7 +793,7 @@ describe('session module', () => {
lastModified: item.getLastModifiedTime(),
eTag: item.getETag(),
}
const downloadResumed = new Promise((resolve) => {
const downloadResumed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
w.webContents.session.once('will-download', function (e, item) {
expect(item.getState()).to.equal('interrupted')
item.setSavePath(downloadFilePath)
@@ -818,9 +820,9 @@ describe('session module', () => {
})
describe('ses.setPermissionRequestHandler(handler)', () => {
afterEach(closeAllWindows)
it('cancels any pending requests when cleared', async () => {
if (w != null) w.destroy()
w = new BrowserWindow({
const w = new BrowserWindow({
show: false,
webPreferences: {
partition: `very-temp-permision-handler`,
@@ -840,7 +842,7 @@ describe('session module', () => {
const result = emittedOnce(require('electron').ipcMain, 'message')
function remote() {
navigator.requestMIDIAccess({sysex: true}).then(() => {}, (err) => {
(navigator as any).requestMIDIAccess({sysex: true}).then(() => {}, (err: any) => {
require('electron').ipcRenderer.send('message', err.name);
});
}
@@ -851,4 +853,31 @@ describe('session module', () => {
expect(name).to.deep.equal('SecurityError')
})
})
describe('ses.setUserAgent()', () => {
afterEach(closeAllWindows)
it('can be retrieved with getUserAgent()', () => {
const userAgent = 'test-agent'
const ses = session.fromPartition(''+Math.random())
ses.setUserAgent(userAgent)
expect(ses.getUserAgent()).to.equal(userAgent)
})
it('sets the User-Agent header for web requests made from renderers', async () => {
const userAgent = 'test-agent'
const ses = session.fromPartition(''+Math.random())
ses.setUserAgent(userAgent)
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } })
let headers: http.IncomingHttpHeaders | null = null
const server = http.createServer((req, res) => {
headers = req.headers
res.end()
server.close()
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)
expect(headers!['user-agent']).to.equal(userAgent)
})
})
})

View File

@@ -1,18 +1,17 @@
const { expect } = require('chai')
const { remote } = require('electron')
const path = require('path')
const http = require('http')
const { emittedNTimes, emittedOnce } = require('./events-helpers')
const { closeWindow } = require('./window-helpers')
const { app, BrowserWindow, ipcMain } = remote
import { expect } from 'chai'
import * as path from 'path'
import * as http from 'http'
import { emittedNTimes, emittedOnce } from './events-helpers'
import { closeWindow } from './window-helpers'
import { app, BrowserWindow, ipcMain } from 'electron'
import { AddressInfo } from 'net'
import { ifdescribe } from './spec-helpers';
describe('renderer nodeIntegrationInSubFrames', () => {
const generateTests = (description, webPreferences) => {
const generateTests = (description: string, webPreferences: any) => {
describe(description, () => {
const fixtureSuffix = webPreferences.webviewTag ? '-webview' : ''
let w
let w: BrowserWindow
beforeEach(async () => {
await closeWindow(w)
@@ -24,10 +23,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
})
})
afterEach(() => {
return closeWindow(w).then(() => {
w = null
})
afterEach(async () => {
await closeWindow(w)
w = null as unknown as BrowserWindow
})
it('should load preload scripts in top level iframes', async () => {
@@ -103,8 +101,8 @@ describe('renderer nodeIntegrationInSubFrames', () => {
})
}
const generateConfigs = (webPreferences, ...permutations) => {
const configs = [{ webPreferences, names: [] }]
const generateConfigs = (webPreferences: any, ...permutations: {name: string, webPreferences: any}[]) => {
const configs = [{ webPreferences, names: [] as string[] }]
for (let i = 0; i < permutations.length; i++) {
const length = configs.length
for (let j = 0; j < length; j++) {
@@ -117,7 +115,7 @@ describe('renderer nodeIntegrationInSubFrames', () => {
}
}
return configs.map(config => {
return configs.map((config: any) => {
if (config.names.length > 0) {
config.title = `with ${config.names.join(', ')} on`
} else {
@@ -125,7 +123,7 @@ describe('renderer nodeIntegrationInSubFrames', () => {
}
delete config.names
return config
return config as {title: string, webPreferences: any}
})
}
@@ -151,7 +149,7 @@ describe('renderer nodeIntegrationInSubFrames', () => {
})
describe('internal <iframe> inside of <webview>', () => {
let w
let w: BrowserWindow
beforeEach(async () => {
await closeWindow(w)
@@ -167,10 +165,9 @@ describe('renderer nodeIntegrationInSubFrames', () => {
})
})
afterEach(() => {
return closeWindow(w).then(() => {
w = null
})
afterEach(async () => {
await closeWindow(w)
w = null as unknown as BrowserWindow
})
it('should not load preload scripts', async () => {
@@ -184,40 +181,36 @@ describe('renderer nodeIntegrationInSubFrames', () => {
})
})
describe('cross-site frame sandboxing', () => {
let server = null
beforeEach(function () {
if (process.platform === 'linux') {
this.skip()
}
})
// app.getAppMetrics() does not return sandbox information on Linux.
ifdescribe(process.platform !== 'linux')('cross-site frame sandboxing', () => {
let server: http.Server
let crossSiteUrl: string
let serverUrl: string
before(function (done) {
server = http.createServer((req, res) => {
res.end(`<iframe name="frame" src="${server.cross_site_url}" />`)
res.end(`<iframe name="frame" src="${crossSiteUrl}" />`)
})
server.listen(0, '127.0.0.1', () => {
server.url = `http://127.0.0.1:${server.address().port}/`
server.cross_site_url = `http://localhost:${server.address().port}/`
serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`
crossSiteUrl = `http://localhost:${(server.address() as AddressInfo).port}/`
done()
})
})
after(() => {
server.close()
server = null
server = null as unknown as http.Server
})
let w
let w: BrowserWindow
afterEach(() => {
return closeWindow(w).then(() => {
w = null
})
afterEach(async () => {
await closeWindow(w)
w = null as unknown as BrowserWindow
})
const generateSpecs = (description, webPreferences) => {
const generateSpecs = (description: string, webPreferences: any) => {
describe(description, () => {
it('iframe process is sandboxed if possible', async () => {
w = new BrowserWindow({
@@ -225,13 +218,13 @@ describe('cross-site frame sandboxing', () => {
webPreferences
})
await w.loadURL(server.url)
await w.loadURL(serverUrl)
const pidMain = w.webContents.getOSProcessId()
const pidFrame = w.webContents._getOSProcessIdForFrame('frame', server.cross_site_url)
const pidFrame = (w.webContents as any)._getOSProcessIdForFrame('frame', crossSiteUrl)
const metrics = app.getAppMetrics()
const isProcessSandboxed = function (pid) {
const isProcessSandboxed = function (pid: number) {
const entry = metrics.filter(metric => metric.pid === pid)[0]
return entry && entry.sandboxed
}

View File

@@ -3,9 +3,10 @@ import { AddressInfo } from 'net'
import * as chaiAsPromised from 'chai-as-promised'
import * as path from 'path'
import * as http from 'http'
import { BrowserWindow, ipcMain, webContents } from 'electron'
import { BrowserWindow, ipcMain, webContents, session } from 'electron'
import { emittedOnce } from './events-helpers';
import { closeAllWindows } from './window-helpers';
import { ifdescribe } from './spec-helpers';
const { expect } = chai
@@ -100,6 +101,7 @@ describe('webContents module', () => {
})
describe('webContents.print()', () => {
afterEach(closeAllWindows)
it('throws when invalid settings are passed', () => {
const w = new BrowserWindow({ show: false })
expect(() => {
@@ -221,4 +223,727 @@ describe('webContents module', () => {
})
})
})
describe('loadURL() promise API', () => {
let w: BrowserWindow
beforeEach(async () => {
w = new BrowserWindow({show: false})
})
afterEach(closeAllWindows)
it('resolves when done loading', async () => {
await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled()
})
it('resolves when done loading a file URL', async () => {
await expect(w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'))).to.eventually.be.fulfilled()
})
it('rejects when failing to load a file URL', async () => {
await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected()
.and.have.property('code', 'ERR_FILE_NOT_FOUND')
})
it('rejects when loading fails due to DNS not resolved', async () => {
await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected()
.and.have.property('code', 'ERR_NAME_NOT_RESOLVED')
})
it('rejects when navigation is cancelled due to a bad scheme', async () => {
await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected()
.and.have.property('code', 'ERR_FAILED')
})
it('sets appropriate error information on rejection', async () => {
let err
try {
await w.loadURL('file:non-existent')
} catch (e) {
err = e
}
expect(err).not.to.be.null()
expect(err.code).to.eql('ERR_FILE_NOT_FOUND')
expect(err.errno).to.eql(-6)
expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent')
})
it('rejects if the load is aborted', async () => {
const s = http.createServer((req, res) => { /* never complete the request */ })
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address() as AddressInfo
const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/)
// load a different file before the first load completes, causing the
// first load to be aborted.
await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'))
await p
s.close()
})
it("doesn't reject when a subframe fails to load", async () => {
let resp = null as unknown as http.ServerResponse
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="http://err.name.not.resolved"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address() as AddressInfo
const p = new Promise(resolve => {
w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.end()
await main
s.close()
})
it("doesn't resolve when a subframe loads", async () => {
let resp = null as unknown as http.ServerResponse
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="data:text/html,hi"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address() as AddressInfo
const p = new Promise(resolve => {
w.webContents.on('did-frame-finish-load', (event, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.destroy() // cause the main request to fail
await expect(main).to.eventually.be.rejected()
.and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING
s.close()
})
})
describe('getFocusedWebContents() API', () => {
afterEach(closeAllWindows)
it('returns the focused web contents', async () => {
const w = new BrowserWindow({show: true})
await w.loadURL('about:blank')
expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id)
const devToolsOpened = emittedOnce(w.webContents, 'devtools-opened')
w.webContents.openDevTools()
await devToolsOpened
expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.devToolsWebContents.id)
const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed')
w.webContents.closeDevTools()
await devToolsClosed
expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id)
})
it('does not crash when called on a detached dev tools window', async () => {
const w = new BrowserWindow({show: true})
w.webContents.openDevTools({ mode: 'detach' })
w.webContents.inspectElement(100, 100)
// For some reason we have to wait for two focused events...?
await emittedOnce(w.webContents, 'devtools-focused')
await emittedOnce(w.webContents, 'devtools-focused')
expect(() => { webContents.getFocusedWebContents() }).to.not.throw()
// Work around https://github.com/electron/electron/issues/19985
await new Promise(r => setTimeout(r, 0))
const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed')
w.webContents.closeDevTools()
await devToolsClosed
expect(() => { webContents.getFocusedWebContents() }).to.not.throw()
})
})
describe('setDevToolsWebContents() API', () => {
afterEach(closeAllWindows)
it('sets arbitrary webContents as devtools', async () => {
const w = new BrowserWindow({ show: false })
const devtools = new BrowserWindow({ show: false })
const promise = emittedOnce(devtools.webContents, 'dom-ready')
w.webContents.setDevToolsWebContents(devtools.webContents)
w.webContents.openDevTools()
await promise
expect(devtools.webContents.getURL().startsWith('devtools://devtools')).to.be.true()
const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name')
expect(result).to.equal('InspectorFrontendHostImpl')
devtools.destroy()
})
})
describe('isFocused() API', () => {
it('returns false when the window is hidden', async () => {
const w = new BrowserWindow({ show: false })
await w.loadURL('about:blank')
expect(w.isVisible()).to.be.false()
expect(w.webContents.isFocused()).to.be.false()
})
})
describe('isCurrentlyAudible() API', () => {
afterEach(closeAllWindows)
it('returns whether audio is playing', async () => {
const w = new BrowserWindow({ show: false })
await w.loadURL('about:blank')
await w.webContents.executeJavaScript(`
window.context = new AudioContext
// Start in suspended state, because of the
// new web audio api policy.
context.suspend()
window.oscillator = context.createOscillator()
oscillator.connect(context.destination)
oscillator.start()
`)
let p = emittedOnce(w.webContents, '-audio-state-changed')
w.webContents.executeJavaScript('context.resume()')
await p
expect(w.webContents.isCurrentlyAudible()).to.be.true()
p = emittedOnce(w.webContents, '-audio-state-changed')
w.webContents.executeJavaScript('oscillator.stop()')
await p
expect(w.webContents.isCurrentlyAudible()).to.be.false()
})
})
describe('getWebPreferences() API', () => {
afterEach(closeAllWindows)
it('should not crash when called for devTools webContents', (done) => {
const w = new BrowserWindow({show: false})
w.webContents.openDevTools()
w.webContents.once('devtools-opened', () => {
expect(w.webContents.devToolsWebContents.getWebPreferences()).to.be.null()
done()
})
})
})
describe('openDevTools() API', () => {
afterEach(closeAllWindows)
it('can show window with activation', async () => {
const w = new BrowserWindow({show: false})
const focused = emittedOnce(w, 'focus')
w.show()
await focused
expect(w.isFocused()).to.be.true()
w.webContents.openDevTools({ mode: 'detach', activate: true })
await emittedOnce(w.webContents, 'devtools-focused')
await emittedOnce(w.webContents, 'devtools-focused')
await emittedOnce(w.webContents, 'devtools-opened')
await new Promise(resolve => setTimeout(resolve, 0))
expect(w.isFocused()).to.be.false()
})
it('can show window without activation', async () => {
const w = new BrowserWindow({show: false})
const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened')
w.webContents.openDevTools({ mode: 'detach', activate: false })
await devtoolsOpened
expect(w.webContents.isDevToolsOpened()).to.be.true()
})
})
describe('before-input-event event', () => {
afterEach(closeAllWindows)
it('can prevent document keyboard events', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html'))
const keyDown = new Promise(resolve => {
ipcMain.once('keydown', (event, key) => resolve(key))
})
w.webContents.once('before-input-event', (event, input) => {
if ('a' === input.key) event.preventDefault()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' })
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' })
expect(await keyDown).to.equal('b')
})
it('has the correct properties', async () => {
const w = new BrowserWindow({show: false})
await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'))
const testBeforeInput = async (opts: any) => {
const modifiers = []
if (opts.shift) modifiers.push('shift')
if (opts.control) modifiers.push('control')
if (opts.alt) modifiers.push('alt')
if (opts.meta) modifiers.push('meta')
if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
const p = emittedOnce(w.webContents, 'before-input-event')
w.webContents.sendInputEvent({
type: opts.type,
keyCode: opts.keyCode,
modifiers: modifiers as any
})
const [, input] = await p
expect(input.type).to.equal(opts.type)
expect(input.key).to.equal(opts.key)
expect(input.code).to.equal(opts.code)
expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat)
expect(input.shift).to.equal(opts.shift)
expect(input.control).to.equal(opts.control)
expect(input.alt).to.equal(opts.alt)
expect(input.meta).to.equal(opts.meta)
}
await testBeforeInput({
type: 'keyDown',
key: 'A',
code: 'KeyA',
keyCode: 'a',
shift: true,
control: true,
alt: true,
meta: true,
isAutoRepeat: true
})
await testBeforeInput({
type: 'keyUp',
key: '.',
code: 'Period',
keyCode: '.',
shift: false,
control: true,
alt: true,
meta: false,
isAutoRepeat: false
})
await testBeforeInput({
type: 'keyUp',
key: '!',
code: 'Digit1',
keyCode: '1',
shift: true,
control: false,
alt: false,
meta: true,
isAutoRepeat: false
})
await testBeforeInput({
type: 'keyUp',
key: 'Tab',
code: 'Tab',
keyCode: 'Tab',
shift: false,
control: true,
alt: false,
meta: false,
isAutoRepeat: true
})
})
})
// On Mac, zooming isn't done with the mouse wheel.
ifdescribe(process.platform !== 'darwin')('zoom-changed', () => {
afterEach(closeAllWindows)
it('is emitted with the correct zooming info', async () => {
const w = new BrowserWindow({ show: false })
await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'))
const testZoomChanged = async ({ zoomingIn }: { zoomingIn: boolean }) => {
w.webContents.sendInputEvent({
type: 'mouseWheel',
x: 300,
y: 300,
deltaX: 0,
deltaY: zoomingIn ? 1 : -1,
wheelTicksX: 0,
wheelTicksY: zoomingIn ? 1 : -1,
modifiers: ['control', 'meta']
})
const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed')
expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out')
// Apparently we get two zoom-changed events??
await emittedOnce(w.webContents, 'zoom-changed')
}
await testZoomChanged({ zoomingIn: true })
await testZoomChanged({ zoomingIn: false })
})
})
describe('sendInputEvent(event)', () => {
let w: BrowserWindow
beforeEach(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html'))
})
afterEach(closeAllWindows)
it('can send keydown events', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('a')
expect(code).to.equal('KeyA')
expect(keyCode).to.equal(65)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
})
it('can send keydown events with modifiers', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Z')
expect(code).to.equal('KeyZ')
expect(keyCode).to.equal(90)
expect(shiftKey).to.be.true()
expect(ctrlKey).to.be.true()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
})
it('can send keydown events with special keys', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Tab')
expect(code).to.equal('Tab')
expect(keyCode).to.equal(9)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.true()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] })
})
it('can send char events', (done) => {
ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('a')
expect(code).to.equal('KeyA')
expect(keyCode).to.equal(65)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' })
})
it('can send char events with modifiers', (done) => {
ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Z')
expect(code).to.equal('KeyZ')
expect(keyCode).to.equal(90)
expect(shiftKey).to.be.true()
expect(ctrlKey).to.be.true()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' })
w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
})
})
describe('insertCSS', () => {
afterEach(closeAllWindows)
it('supports inserting CSS', async () => {
const w = new BrowserWindow({ show: false })
w.loadURL('about:blank')
await w.webContents.insertCSS('body { background-repeat: round; }')
const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
expect(result).to.equal('round')
})
it('supports removing inserted CSS', async () => {
const w = new BrowserWindow({ show: false })
w.loadURL('about:blank')
const key = await w.webContents.insertCSS('body { background-repeat: round; }')
await w.webContents.removeInsertedCSS(key)
const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
expect(result).to.equal('repeat')
})
})
describe('inspectElement()', () => {
afterEach(closeAllWindows)
it('supports inspecting an element in the devtools', (done) => {
const w = new BrowserWindow({ show: false })
w.loadURL('about:blank')
w.webContents.once('devtools-opened', () => { done() })
w.webContents.inspectElement(10, 10)
})
})
describe('startDrag({file, icon})', () => {
it('throws errors for a missing file or a missing/empty icon', () => {
const w = new BrowserWindow({ show: false })
expect(() => {
w.webContents.startDrag({ icon: path.join(fixturesPath, 'assets', 'logo.png') } as any)
}).to.throw(`Must specify either 'file' or 'files' option`)
expect(() => {
w.webContents.startDrag({ file: __filename } as any)
}).to.throw(`Must specify 'icon' option`)
if (process.platform === 'darwin') {
expect(() => {
w.webContents.startDrag({ file: __filename, icon: __filename })
}).to.throw(`Must specify non-empty 'icon' option`)
}
})
})
describe('focus()', () => {
describe('when the web contents is hidden', () => {
afterEach(closeAllWindows)
it('does not blur the focused window', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
ipcMain.once('answer', (event, parentFocused, childFocused) => {
expect(parentFocused).to.be.true()
expect(childFocused).to.be.false()
done()
})
w.show()
w.loadFile(path.join(fixturesPath, 'pages', 'focus-web-contents.html'))
})
})
})
describe('getOSProcessId()', () => {
afterEach(closeAllWindows)
it('returns a valid procress id', async () => {
const w = new BrowserWindow({ show: false })
expect(w.webContents.getOSProcessId()).to.equal(0)
await w.loadURL('about:blank')
expect(w.webContents.getOSProcessId()).to.be.above(0)
})
})
describe('zoom api', () => {
const scheme = (global as any).standardScheme
const hostZoomMap: Record<string, number> = {
host1: 0.3,
host2: 0.7,
host3: 0.2
}
before((done) => {
const protocol = session.defaultSession.protocol
protocol.registerStringProtocol(scheme, (request, callback) => {
const response = `<script>
const {ipcRenderer, remote} = require('electron')
ipcRenderer.send('set-zoom', window.location.hostname)
ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
const { zoomLevel } = remote.getCurrentWebContents()
ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
})
</script>`
callback({ data: response, mimeType: 'text/html' })
}, (error) => done(error))
})
after((done) => {
const protocol = session.defaultSession.protocol
protocol.unregisterProtocol(scheme, (error) => done(error))
})
afterEach(closeAllWindows)
// TODO(codebytere): remove in Electron v8.0.0
it('can set the correct zoom level (functions)', async () => {
const w = new BrowserWindow({ show: false })
try {
await w.loadURL('about:blank')
const zoomLevel = w.webContents.getZoomLevel()
expect(zoomLevel).to.eql(0.0)
w.webContents.setZoomLevel(0.5)
const newZoomLevel = w.webContents.getZoomLevel()
expect(newZoomLevel).to.eql(0.5)
} finally {
w.webContents.setZoomLevel(0)
}
})
it('can set the correct zoom level', async () => {
const w = new BrowserWindow({ show: false })
try {
await w.loadURL('about:blank')
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.eql(0.0)
w.webContents.zoomLevel = 0.5
const newZoomLevel = w.webContents.zoomLevel
expect(newZoomLevel).to.eql(0.5)
} finally {
w.webContents.zoomLevel = 0
}
})
it('can persist zoom level across navigation', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
let finalNavigation = false
ipcMain.on('set-zoom', (e, host) => {
const zoomLevel = hostZoomMap[host]
if (!finalNavigation) w.webContents.zoomLevel = zoomLevel
e.sender.send(`${host}-zoom-set`)
})
ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
const expectedZoomLevel = hostZoomMap.host1
expect(zoomLevel).to.equal(expectedZoomLevel)
if (finalNavigation) {
done()
} else {
w.loadURL(`${scheme}://host2`)
}
})
ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
const expectedZoomLevel = hostZoomMap.host2
expect(zoomLevel).to.equal(expectedZoomLevel)
finalNavigation = true
w.webContents.goBack()
})
w.loadURL(`${scheme}://host1`)
})
it('can propagate zoom level across same session', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
const w2 = new BrowserWindow({ show: false })
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(hostZoomMap.host3)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel1).to.equal(zoomLevel2)
w2.setClosable(true)
w2.close()
done()
})
w.webContents.on('did-finish-load', () => {
w.webContents.zoomLevel = hostZoomMap.host3
w2.loadURL(`${scheme}://host3`)
})
w.loadURL(`${scheme}://host3`)
})
it('cannot propagate zoom level across different session', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
const w2 = new BrowserWindow({
show: false,
webPreferences: {
partition: 'temp'
}
})
const protocol = w2.webContents.session.protocol
protocol.registerStringProtocol(scheme, (request, callback) => {
callback('hello')
}, (error) => {
if (error) return done(error)
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(hostZoomMap.host3)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel2).to.equal(0)
expect(zoomLevel1).to.not.equal(zoomLevel2)
protocol.unregisterProtocol(scheme, (error) => {
if (error) return done(error)
w2.setClosable(true)
w2.close()
done()
})
})
w.webContents.on('did-finish-load', () => {
w.webContents.zoomLevel = hostZoomMap.host3
w2.loadURL(`${scheme}://host3`)
})
w.loadURL(`${scheme}://host3`)
})
})
it('can persist when it contains iframe', (done) => {
const w = new BrowserWindow({ show: false })
const server = http.createServer((req, res) => {
setTimeout(() => {
res.end()
}, 200)
})
server.listen(0, '127.0.0.1', () => {
const url = 'http://127.0.0.1:' + (server.address() as AddressInfo).port
const content = `<iframe src=${url}></iframe>`
w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
if (!isMainFrame) {
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.equal(2.0)
w.webContents.zoomLevel = 0
server.close()
done()
}
})
w.webContents.on('dom-ready', () => {
w.webContents.zoomLevel = 2.0
})
w.loadURL(`data:text/html,${content}`)
})
})
it('cannot propagate when used with webframe', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
let finalZoomLevel = 0
const w2 = new BrowserWindow({
show: false
})
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(finalZoomLevel)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel2).to.equal(0)
expect(zoomLevel1).to.not.equal(zoomLevel2)
w2.setClosable(true)
w2.close()
done()
})
ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
w2.loadFile(path.join(fixturesPath, 'pages', 'c.html'))
finalZoomLevel = zoomLevel
})
w.loadFile(path.join(fixturesPath, 'pages', 'webframe-zoom.html'))
})
it('cannot persist zoom level after navigation with webFrame', (done) => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
let initialNavigation = true
const source = `
const {ipcRenderer, webFrame} = require('electron')
webFrame.setZoomLevel(0.6)
ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
`
w.webContents.on('did-finish-load', () => {
if (initialNavigation) {
w.webContents.executeJavaScript(source)
} else {
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.equal(0)
done()
}
})
ipcMain.once('zoom-level-set', (e, zoomLevel) => {
expect(zoomLevel).to.equal(0.6)
w.loadFile(path.join(fixturesPath, 'pages', 'd.html'))
initialNavigation = false
})
w.loadFile(path.join(fixturesPath, 'pages', 'c.html'))
})
})
})

View File

@@ -1,30 +1,23 @@
'use strict'
import { expect } from 'chai'
import * as ChildProcess from 'child_process'
import * as path from 'path'
import { emittedOnce } from './events-helpers'
import { closeWindow } from './window-helpers'
const chai = require('chai')
const ChildProcess = require('child_process')
const dirtyChai = require('dirty-chai')
const path = require('path')
const { emittedOnce } = require('./events-helpers')
const { closeWindow } = require('./window-helpers')
const { remote } = require('electron')
const { webContents, TopLevelWindow, WebContentsView } = remote
const { expect } = chai
chai.use(dirtyChai)
import { webContents, TopLevelWindow, WebContentsView } from 'electron'
describe('WebContentsView', () => {
let w = null
afterEach(() => closeWindow(w).then(() => { w = null }))
let w: TopLevelWindow
afterEach(() => closeWindow(w as any).then(() => { w = null as unknown as TopLevelWindow }))
it('can be used as content view', () => {
const web = webContents.create({})
const web = (webContents as any).create({})
w = new TopLevelWindow({ show: false })
w.setContentView(new WebContentsView(web))
})
it('prevents adding same WebContents', () => {
const web = webContents.create({})
const web = (webContents as any).create({})
w = new TopLevelWindow({ show: false })
w.setContentView(new WebContentsView(web))
expect(() => {
@@ -35,7 +28,7 @@ describe('WebContentsView', () => {
describe('new WebContentsView()', () => {
it('does not crash on exit', async () => {
const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontentsview.js')
const electronPath = remote.getGlobal('process').execPath
const electronPath = process.execPath
const appProcess = ChildProcess.spawn(electronPath, [appPath])
const [code] = await emittedOnce(appProcess, 'close')
expect(code).to.equal(0)

View File

@@ -1,16 +1,11 @@
const chai = require('chai')
const dirtyChai = require('dirty-chai')
import { expect } from 'chai'
import * as http from 'http'
import * as qs from 'querystring'
import * as path from 'path'
import { session, WebContents, webContents } from 'electron'
import { AddressInfo } from 'net';
const http = require('http')
const qs = require('querystring')
const remote = require('electron').remote
const session = remote.session
const { expect } = chai
chai.use(dirtyChai)
/* The whole webRequest API doesn't use standard callbacks */
/* eslint-disable standard/no-callback-literal */
const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures')
describe('webRequest module', () => {
const ses = session.defaultSession
@@ -28,12 +23,12 @@ describe('webRequest module', () => {
res.end(content)
}
})
let defaultURL = null
let defaultURL: string
before((done) => {
server.listen(0, '127.0.0.1', () => {
const port = server.address().port
defaultURL = 'http://127.0.0.1:' + port + '/'
const port = (server.address() as AddressInfo).port
defaultURL = `http://127.0.0.1:${port}/`
done()
})
})
@@ -42,48 +37,43 @@ describe('webRequest module', () => {
server.close()
})
let contents: WebContents = null as unknown as WebContents
// NB. sandbox: true is used because it makes navigations much (~8x) faster.
before(async () => {
contents = (webContents as any).create({sandbox: true})
await contents.loadFile(path.join(fixturesPath, 'pages', 'jquery.html'))
})
after(() => (contents as any).destroy())
async function ajax (url: string, options = {}) {
return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`)
}
describe('webRequest.onBeforeRequest', () => {
afterEach(() => {
ses.webRequest.onBeforeRequest(null)
})
it('can cancel the request', (done) => {
it('can cancel the request', async () => {
ses.webRequest.onBeforeRequest((details, callback) => {
callback({
cancel: true
})
})
$.ajax({
url: defaultURL,
success: () => {
done('unexpected success')
},
error: () => {
done()
}
})
await expect(ajax(defaultURL)).to.eventually.be.rejectedWith('404')
})
it('can filter URLs', (done) => {
it('can filter URLs', async () => {
const filter = { urls: [defaultURL + 'filter/*'] }
ses.webRequest.onBeforeRequest(filter, (details, callback) => {
callback({ cancel: true })
})
$.ajax({
url: `${defaultURL}nofilter/test`,
success: (data) => {
expect(data).to.equal('/nofilter/test')
$.ajax({
url: `${defaultURL}filter/test`,
success: () => done('unexpected success'),
error: () => done()
})
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(`${defaultURL}nofilter/test`)
expect(data).to.equal('/nofilter/test')
await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejectedWith('404')
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onBeforeRequest((details, callback) => {
expect(details.id).to.be.a('number')
expect(details.timestamp).to.be.a('number')
@@ -94,17 +84,11 @@ describe('webRequest module', () => {
expect(details.uploadData).to.be.undefined()
callback({})
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/')
})
it('receives post data in details object', (done) => {
it('receives post data in details object', async () => {
const postData = {
name: 'post test',
type: 'string'
@@ -117,16 +101,13 @@ describe('webRequest module', () => {
expect(data).to.deep.equal(postData)
callback({ cancel: true })
})
$.ajax({
url: defaultURL,
await expect(ajax(defaultURL, {
type: 'POST',
data: postData,
success: () => {},
error: () => done()
})
})).to.eventually.be.rejectedWith('404')
})
it('can redirect the request', (done) => {
it('can redirect the request', async () => {
ses.webRequest.onBeforeRequest((details, callback) => {
if (details.url === defaultURL) {
callback({ redirectURL: `${defaultURL}redirect` })
@@ -134,14 +115,8 @@ describe('webRequest module', () => {
callback({})
}
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/redirect')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/redirect')
})
})
@@ -150,40 +125,27 @@ describe('webRequest module', () => {
ses.webRequest.onBeforeSendHeaders(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onBeforeSendHeaders((details, callback) => {
expect(details.requestHeaders).to.be.an('object')
expect(details.requestHeaders['Foo.Bar']).to.equal('baz')
callback({})
})
$.ajax({
url: defaultURL,
headers: { 'Foo.Bar': 'baz' },
success: (data) => {
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL, { headers: { 'Foo.Bar': 'baz' } })
expect(data).to.equal('/')
})
it('can change the request headers', (done) => {
it('can change the request headers', async () => {
ses.webRequest.onBeforeSendHeaders((details, callback) => {
const requestHeaders = details.requestHeaders
requestHeaders.Accept = '*/*;test/header'
callback({ requestHeaders: requestHeaders })
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/header/received')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/header/received')
})
it('resets the whole headers', (done) => {
it('resets the whole headers', async () => {
const requestHeaders = {
Test: 'header'
}
@@ -192,12 +154,8 @@ describe('webRequest module', () => {
})
ses.webRequest.onSendHeaders((details) => {
expect(details.requestHeaders).to.deep.equal(requestHeaders)
done()
})
$.ajax({
url: defaultURL,
error: (xhr, errorType) => done(errorType)
})
await ajax(defaultURL)
})
})
@@ -206,18 +164,12 @@ describe('webRequest module', () => {
ses.webRequest.onSendHeaders(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onSendHeaders((details) => {
expect(details.requestHeaders).to.be.an('object')
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/')
})
})
@@ -226,71 +178,46 @@ describe('webRequest module', () => {
ses.webRequest.onHeadersReceived(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onHeadersReceived((details, callback) => {
expect(details.statusLine).to.equal('HTTP/1.1 200 OK')
expect(details.statusCode).to.equal(200)
expect(details.responseHeaders['Custom']).to.deep.equal(['Header'])
expect(details.responseHeaders!['Custom']).to.deep.equal(['Header'])
callback({})
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/')
})
it('can change the response header', (done) => {
it('can change the response header', async () => {
ses.webRequest.onHeadersReceived((details, callback) => {
const responseHeaders = details.responseHeaders
responseHeaders['Custom'] = ['Changed']
const responseHeaders = details.responseHeaders!
responseHeaders['Custom'] = ['Changed'] as any
callback({ responseHeaders: responseHeaders })
})
$.ajax({
url: defaultURL,
success: (data, status, xhr) => {
expect(xhr.getResponseHeader('Custom')).to.equal('Changed')
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { headers } = await ajax(defaultURL)
expect(headers).to.match(/^custom: Changed$/m)
})
it('does not change header by default', (done) => {
it('does not change header by default', async () => {
ses.webRequest.onHeadersReceived((details, callback) => {
callback({})
})
$.ajax({
url: defaultURL,
success: (data, status, xhr) => {
expect(xhr.getResponseHeader('Custom')).to.equal('Header')
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data, headers } = await ajax(defaultURL)
expect(headers).to.match(/^custom: Header$/m)
expect(data).to.equal('/')
})
it('follows server redirect', (done) => {
it('follows server redirect', async () => {
ses.webRequest.onHeadersReceived((details, callback) => {
const responseHeaders = details.responseHeaders
callback({ responseHeaders: responseHeaders })
})
$.ajax({
url: defaultURL + 'serverRedirect',
success: (data, status, xhr) => {
expect(xhr.getResponseHeader('Custom')).to.equal('Header')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { headers } = await ajax(defaultURL + 'serverRedirect')
expect(headers).to.match(/^custom: Header$/m)
})
it('can change the header status', (done) => {
it('can change the header status', async () => {
ses.webRequest.onHeadersReceived((details, callback) => {
const responseHeaders = details.responseHeaders
callback({
@@ -298,14 +225,19 @@ describe('webRequest module', () => {
statusLine: 'HTTP/1.1 404 Not Found'
})
})
$.ajax({
url: defaultURL,
success: (data, status, xhr) => {},
error: (xhr, errorType) => {
expect(xhr.getResponseHeader('Custom')).to.equal('Header')
done()
const { headers } = await contents.executeJavaScript(`new Promise((resolve, reject) => {
const options = {
...${JSON.stringify({url: defaultURL})},
success: (data, status, request) => {
reject(new Error('expected failure'))
},
error: (xhr) => {
resolve({ headers: xhr.getAllResponseHeaders() })
}
}
})
$.ajax(options)
})`)
expect(headers).to.match(/^custom: Header$/m)
})
})
@@ -314,22 +246,16 @@ describe('webRequest module', () => {
ses.webRequest.onResponseStarted(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onResponseStarted((details) => {
expect(details.fromCache).to.be.a('boolean')
expect(details.statusLine).to.equal('HTTP/1.1 200 OK')
expect(details.statusCode).to.equal(200)
expect(details.responseHeaders['Custom']).to.deep.equal(['Header'])
})
$.ajax({
url: defaultURL,
success: (data, status, xhr) => {
expect(xhr.getResponseHeader('Custom')).to.equal('Header')
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
expect(details.responseHeaders!['Custom']).to.deep.equal(['Header'])
})
const { data, headers } = await ajax(defaultURL)
expect(headers).to.match(/^custom: Header$/m)
expect(data).to.equal('/')
})
})
@@ -339,7 +265,7 @@ describe('webRequest module', () => {
ses.webRequest.onBeforeRequest(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
const redirectURL = defaultURL + 'redirect'
ses.webRequest.onBeforeRequest((details, callback) => {
if (details.url === defaultURL) {
@@ -354,14 +280,8 @@ describe('webRequest module', () => {
expect(details.statusCode).to.equal(307)
expect(details.redirectURL).to.equal(redirectURL)
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/redirect')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/redirect')
})
})
@@ -370,20 +290,14 @@ describe('webRequest module', () => {
ses.webRequest.onCompleted(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onCompleted((details) => {
expect(details.fromCache).to.be.a('boolean')
expect(details.statusLine).to.equal('HTTP/1.1 200 OK')
expect(details.statusCode).to.equal(200)
})
$.ajax({
url: defaultURL,
success: (data) => {
expect(data).to.equal('/')
done()
},
error: (xhr, errorType) => done(errorType)
})
const { data } = await ajax(defaultURL)
expect(data).to.equal('/')
})
})
@@ -393,18 +307,14 @@ describe('webRequest module', () => {
ses.webRequest.onBeforeRequest(null)
})
it('receives details object', (done) => {
it('receives details object', async () => {
ses.webRequest.onBeforeRequest((details, callback) => {
callback({ cancel: true })
})
ses.webRequest.onErrorOccurred((details) => {
expect(details.error).to.equal('net::ERR_BLOCKED_BY_CLIENT')
done()
})
$.ajax({
url: defaultURL,
success: () => done('unexpected success')
})
await expect(ajax(defaultURL)).to.eventually.be.rejectedWith('404')
})
})
})

View File

@@ -1,17 +1,16 @@
const { expect } = require('chai')
const { remote } = require('electron')
const path = require('path')
import { expect } from 'chai'
import * as path from 'path'
const { closeWindow } = require('./window-helpers')
const { emittedNTimes } = require('./events-helpers')
import { closeWindow } from './window-helpers'
import { emittedNTimes } from './events-helpers'
const { BrowserWindow, ipcMain } = remote
import { BrowserWindow, ipcMain, WebContents } from 'electron'
describe('chrome extension content scripts', () => {
const fixtures = path.resolve(__dirname, 'fixtures')
const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures')
const extensionPath = path.resolve(fixtures, 'extensions')
const addExtension = (name) => BrowserWindow.addExtension(path.resolve(extensionPath, name))
const addExtension = (name: string) => BrowserWindow.addExtension(path.resolve(extensionPath, name))
const removeAllExtensions = () => {
Object.keys(BrowserWindow.getExtensions()).map(extName => {
BrowserWindow.removeExtension(extName)
@@ -19,7 +18,7 @@ describe('chrome extension content scripts', () => {
}
let responseIdCounter = 0
const executeJavaScriptInFrame = (webContents, frameRoutingId, code) => {
const executeJavaScriptInFrame = (webContents: WebContents, frameRoutingId: number, code: string) => {
return new Promise(resolve => {
const responseId = responseIdCounter++
ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => {
@@ -29,9 +28,9 @@ describe('chrome extension content scripts', () => {
})
}
const generateTests = (sandboxEnabled, contextIsolationEnabled) => {
const generateTests = (sandboxEnabled: boolean, contextIsolationEnabled: boolean) => {
describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => {
let w
let w: BrowserWindow
describe('supports "run_at" option', () => {
beforeEach(async () => {
@@ -49,7 +48,7 @@ describe('chrome extension content scripts', () => {
afterEach(() => {
removeAllExtensions()
return closeWindow(w).then(() => { w = null })
return closeWindow(w).then(() => { w = null as unknown as BrowserWindow })
})
it('should run content script at document_start', () => {
@@ -107,7 +106,7 @@ describe('chrome extension content scripts', () => {
afterEach(() =>
closeWindow(w).then(() => {
w = null
w = null as unknown as BrowserWindow
})
)
@@ -118,7 +117,7 @@ describe('chrome extension content scripts', () => {
await Promise.all(
frameEvents.map(async frameEvent => {
const [, isMainFrame, , frameRoutingId] = frameEvent
const result = await executeJavaScriptInFrame(
const result: any = await executeJavaScriptInFrame(
w.webContents,
frameRoutingId,
`(() => {

View File

@@ -25,8 +25,10 @@ app.on('window-all-closed', () => null)
app.commandLine.appendSwitch('ignore-certificate-errors')
global.standardScheme = 'app'
global.zoomScheme = 'zoom'
protocol.registerSchemesAsPrivileged([
{ scheme: global.standardScheme, privileges: { standard: true, secure: true } },
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },

View File

@@ -1,12 +1,11 @@
const chai = require('chai')
const { expect } = chai
import { expect } from 'chai'
describe('feature-string parsing', () => {
it('is indifferent to whitespace around keys and values', () => {
const parseFeaturesString = require('../lib/common/parse-features-string')
const checkParse = (string, parsed) => {
const features = {}
parseFeaturesString(string, (k, v) => { features[k] = v })
const checkParse = (string: string, parsed: Record<string, string | boolean>) => {
const features: Record<string, string | boolean> = {}
parseFeaturesString(string, (k: string, v: any) => { features[k] = v })
expect(features).to.deep.equal(parsed)
}
checkParse('a=yes,c=d', { a: true, c: 'd' })

View File

@@ -0,0 +1,235 @@
import { expect } from 'chai'
import * as http from 'http'
import * as fs from 'fs'
import * as path from 'path'
import * as url from 'url'
import { BrowserWindow, WebPreferences } from 'electron'
import { closeWindow } from './window-helpers'
import { AddressInfo } from 'net';
import { emittedOnce } from './events-helpers';
describe('security warnings', () => {
let server: http.Server
let w: BrowserWindow
let useCsp = true
let serverUrl: string
before((done) => {
// Create HTTP Server
server = http.createServer((request, response) => {
const uri = url.parse(request.url!).pathname!
let filename = path.join(__dirname, '..', 'spec', 'fixtures', 'pages', uri)
fs.stat(filename, (error, stats) => {
if (error) {
response.writeHead(404, { 'Content-Type': 'text/plain' })
response.end()
return
}
if (stats.isDirectory()) {
filename += '/index.html'
}
fs.readFile(filename, 'binary', (err, file) => {
if (err) {
response.writeHead(404, { 'Content-Type': 'text/plain' })
response.end()
return
}
const cspHeaders = { 'Content-Security-Policy': `script-src 'self' 'unsafe-inline'` }
response.writeHead(200, useCsp ? cspHeaders : undefined)
response.write(file, 'binary')
response.end()
})
})
}).listen(0, '127.0.0.1', () => {
serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
done()
})
})
after(() => {
// Close server
server.close()
server = null as unknown as any
})
afterEach(async () => {
useCsp = true
await closeWindow(w)
w = null as unknown as any
})
it('should warn about Node.js integration with remote content', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true
}
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('Node.js Integration with Remote Content')
})
it('should not warn about Node.js integration with remote content from localhost', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.not.include('Node.js Integration with Remote Content')
if (message === 'loaded') {
done()
}
})
w.loadURL(`${serverUrl}/base-page-security-onload-message.html`)
})
const generateSpecs = (description: string, webPreferences: WebPreferences) => {
describe(description, () => {
it('should warn about disabled webSecurity', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
webSecurity: false,
...webPreferences
}
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('Disabled webSecurity')
})
it('should warn about insecure Content-Security-Policy', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
enableRemoteModule: false,
...webPreferences
}
})
useCsp = false
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('Insecure Content-Security-Policy')
})
it('should warn about allowRunningInsecureContent', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
allowRunningInsecureContent: true,
...webPreferences
}
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('allowRunningInsecureContent')
})
it('should warn about experimentalFeatures', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
experimentalFeatures: true,
...webPreferences
}
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('experimentalFeatures')
})
it('should warn about enableBlinkFeatures', async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
enableBlinkFeatures: 'my-cool-feature',
...webPreferences
}
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('enableBlinkFeatures')
})
it('should warn about allowpopups', async () => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.loadURL(`${serverUrl}/webview-allowpopups.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('allowpopups')
})
it('should warn about insecure resources', async () => {
w = new BrowserWindow({
show: false,
webPreferences: { ...webPreferences }
})
w.loadURL(`${serverUrl}/insecure-resources.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('Insecure Resources')
})
it('should not warn about loading insecure-resources.html from localhost', async () => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.loadURL(`${serverUrl}/insecure-resources.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.not.include('insecure-resources.html')
})
it('should warn about enabled remote module with remote content', async () => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.loadURL(`${serverUrl}/base-page-security.html`)
const [,, message] = await emittedOnce(w.webContents, 'console-message')
expect(message).to.include('enableRemoteModule')
})
it('should not warn about enabled remote module with remote content from localhost', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.not.include('enableRemoteModule')
if (message === 'loaded') {
done()
}
})
w.loadURL(`${serverUrl}/base-page-security-onload-message.html`)
})
})
}
generateSpecs('without sandbox', {})
generateSpecs('with sandbox', { sandbox: true })
})

View File

@@ -4,10 +4,12 @@ const dirtyChai = require('dirty-chai')
const fs = require('fs')
const path = require('path')
const os = require('os')
const http = require('http')
const { shell, remote } = require('electron')
const { BrowserWindow } = remote
const { closeWindow } = require('./window-helpers')
const { emittedOnce } = require('./events-helpers')
const { expect } = chai
chai.use(dirtyChai)
@@ -47,32 +49,34 @@ describe('shell module', () => {
}
})
it('opens an external link', done => {
const url = 'http://www.example.com'
it('opens an external link', async () => {
let url = 'http://127.0.0.1'
let requestReceived
if (process.platform === 'linux') {
process.env.BROWSER = '/bin/true'
process.env.DE = 'generic'
process.env.DISPLAY = ''
requestReceived = Promise.resolve()
} else if (process.platform === 'darwin') {
// On the Mac CI machines, Safari tries to ask for a password to the
// code signing keychain we set up to test code signing (see
// https://github.com/electron/electron/pull/19969#issuecomment-526278890),
// so use a blur event as a crude proxy.
w = new BrowserWindow({ show: true })
requestReceived = emittedOnce(w, 'blur')
} else {
const server = http.createServer((req, res) => {
res.end()
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
requestReceived = new Promise(resolve => server.on('connection', () => resolve()))
url = `http://127.0.0.1:${server.address().port}`
}
// Ensure an external window is activated via a new window's blur event
w = new BrowserWindow()
let promiseResolved = false
let blurEventEmitted = false
w.on('blur', () => {
blurEventEmitted = true
if (promiseResolved) {
done()
}
})
shell.openExternal(url).then(() => {
promiseResolved = true
if (blurEventEmitted || process.platform === 'linux') {
done()
}
})
await Promise.all([
shell.openExternal(url),
requestReceived
])
})
})

View File

@@ -40,347 +40,6 @@ describe('webContents module', () => {
afterEach(() => closeWindow(w).then(() => { w = null }))
describe('loadURL() promise API', () => {
it('resolves when done loading', async () => {
await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled
})
it('resolves when done loading a file URL', async () => {
await expect(w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))).to.eventually.be.fulfilled
})
it('rejects when failing to load a file URL', async () => {
await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected
.and.have.property('code', 'ERR_FILE_NOT_FOUND')
})
it('rejects when loading fails due to DNS not resolved', async () => {
await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected
.and.have.property('code', 'ERR_NAME_NOT_RESOLVED')
})
it('rejects when navigation is cancelled due to a bad scheme', async () => {
await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected
.and.have.property('code', 'ERR_FAILED')
})
it('sets appropriate error information on rejection', async () => {
let err
try {
await w.loadURL('file:non-existent')
} catch (e) {
err = e
}
expect(err).not.to.be.null()
expect(err.code).to.eql('ERR_FILE_NOT_FOUND')
expect(err.errno).to.eql(-6)
expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent')
})
it('rejects if the load is aborted', async () => {
const s = http.createServer((req, res) => { /* never complete the request */ })
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/)
// load a different file before the first load completes, causing the
// first load to be aborted.
await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
await p
s.close()
})
it("doesn't reject when a subframe fails to load", async () => {
let resp = null
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="http://err.name.not.resolved"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = new Promise(resolve => {
w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.end()
await main
s.close()
})
it("doesn't resolve when a subframe loads", async () => {
let resp = null
const s = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.write('<iframe src="data:text/html,hi"></iframe>')
resp = res
// don't end the response yet
})
await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
const { port } = s.address()
const p = new Promise(resolve => {
w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
if (!isMainFrame) {
resolve()
}
})
})
const main = w.loadURL(`http://127.0.0.1:${port}`)
await p
resp.destroy() // cause the main request to fail
await expect(main).to.eventually.be.rejected
.and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING
s.close()
})
})
describe('getFocusedWebContents() API', () => {
it('returns the focused web contents', (done) => {
if (isCi) return done()
const specWebContents = remote.getCurrentWebContents()
expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id)
specWebContents.once('devtools-opened', () => {
expect(specWebContents.devToolsWebContents.id).to.equal(webContents.getFocusedWebContents().id)
specWebContents.closeDevTools()
})
specWebContents.once('devtools-closed', () => {
expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id)
done()
})
specWebContents.openDevTools()
})
it('does not crash when called on a detached dev tools window', (done) => {
const specWebContents = w.webContents
specWebContents.once('devtools-opened', () => {
expect(() => {
webContents.getFocusedWebContents()
}).to.not.throw()
specWebContents.closeDevTools()
})
specWebContents.once('devtools-closed', () => {
expect(() => {
webContents.getFocusedWebContents()
}).to.not.throw()
done()
})
specWebContents.openDevTools({ mode: 'detach' })
w.inspectElement(100, 100)
})
})
describe('setDevToolsWebContents() API', () => {
it('sets arbitrary webContents as devtools', async () => {
const devtools = new BrowserWindow({ show: false })
const promise = emittedOnce(devtools.webContents, 'dom-ready')
w.webContents.setDevToolsWebContents(devtools.webContents)
w.webContents.openDevTools()
await promise
expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true()
const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name')
expect(result).to.equal('InspectorFrontendHostImpl')
devtools.destroy()
})
})
describe('isFocused() API', () => {
it('returns false when the window is hidden', () => {
BrowserWindow.getAllWindows().forEach((window) => {
expect(!window.isVisible() && window.webContents.isFocused()).to.be.false()
})
})
})
describe('isCurrentlyAudible() API', () => {
it('returns whether audio is playing', async () => {
const webContents = remote.getCurrentWebContents()
const context = new window.AudioContext()
// Start in suspended state, because of the
// new web audio api policy.
context.suspend()
const oscillator = context.createOscillator()
oscillator.connect(context.destination)
oscillator.start()
let p = emittedOnce(webContents, '-audio-state-changed')
await context.resume()
await p
expect(webContents.isCurrentlyAudible()).to.be.true()
p = emittedOnce(webContents, '-audio-state-changed')
oscillator.stop()
await p
expect(webContents.isCurrentlyAudible()).to.be.false()
oscillator.disconnect()
context.close()
})
})
describe('getWebPreferences() API', () => {
it('should not crash when called for devTools webContents', (done) => {
w.webContents.openDevTools()
w.webContents.once('devtools-opened', () => {
expect(w.devToolsWebContents.getWebPreferences()).to.be.null()
done()
})
})
})
describe('openDevTools() API', () => {
it('can show window with activation', async () => {
const focused = emittedOnce(w, 'focus')
w.show()
await focused
expect(w.isFocused()).to.be.true()
const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened')
w.webContents.openDevTools({ mode: 'detach', activate: true })
await devtoolsOpened
expect(w.isFocused()).to.be.false()
})
it('can show window without activation', async () => {
const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened')
w.webContents.openDevTools({ mode: 'detach', activate: false })
await devtoolsOpened
expect(w.isDevToolsOpened()).to.be.true()
})
})
describe('before-input-event event', () => {
it('can prevent document keyboard events', async () => {
await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
const keyDown = new Promise(resolve => {
ipcMain.once('keydown', (event, key) => resolve(key))
})
ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id)
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' })
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' })
expect(await keyDown).to.equal('b')
})
it('has the correct properties', async () => {
await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
const testBeforeInput = async (opts) => {
const modifiers = []
if (opts.shift) modifiers.push('shift')
if (opts.control) modifiers.push('control')
if (opts.alt) modifiers.push('alt')
if (opts.meta) modifiers.push('meta')
if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
const p = emittedOnce(w.webContents, 'before-input-event')
w.webContents.sendInputEvent({
type: opts.type,
keyCode: opts.keyCode,
modifiers: modifiers
})
const [, input] = await p
expect(input.type).to.equal(opts.type)
expect(input.key).to.equal(opts.key)
expect(input.code).to.equal(opts.code)
expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat)
expect(input.shift).to.equal(opts.shift)
expect(input.control).to.equal(opts.control)
expect(input.alt).to.equal(opts.alt)
expect(input.meta).to.equal(opts.meta)
}
await testBeforeInput({
type: 'keyDown',
key: 'A',
code: 'KeyA',
keyCode: 'a',
shift: true,
control: true,
alt: true,
meta: true,
isAutoRepeat: true
})
await testBeforeInput({
type: 'keyUp',
key: '.',
code: 'Period',
keyCode: '.',
shift: false,
control: true,
alt: true,
meta: false,
isAutoRepeat: false
})
await testBeforeInput({
type: 'keyUp',
key: '!',
code: 'Digit1',
keyCode: '1',
shift: true,
control: false,
alt: false,
meta: true,
isAutoRepeat: false
})
await testBeforeInput({
type: 'keyUp',
key: 'Tab',
code: 'Tab',
keyCode: 'Tab',
shift: false,
control: true,
alt: false,
meta: false,
isAutoRepeat: true
})
})
})
describe('zoom-changed', () => {
beforeEach(function () {
// On Mac, zooming isn't done with the mouse wheel.
if (process.platform === 'darwin') {
return closeWindow(w).then(() => {
w = null
this.skip()
})
}
})
it('is emitted with the correct zooming info', async () => {
w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
await emittedOnce(w.webContents, 'did-finish-load')
const testZoomChanged = async ({ zoomingIn }) => {
const promise = emittedOnce(w.webContents, 'zoom-changed')
w.webContents.sendInputEvent({
type: 'mousewheel',
x: 300,
y: 300,
deltaX: 0,
deltaY: zoomingIn ? 1 : -1,
wheelTicksX: 0,
wheelTicksY: zoomingIn ? 1 : -1,
phase: 'began',
modifiers: ['control', 'meta']
})
const [, zoomDirection] = await promise
expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out')
}
await testZoomChanged({ zoomingIn: true })
await testZoomChanged({ zoomingIn: false })
})
})
describe('devtools window', () => {
let testFn = it
if (process.platform === 'darwin' && isCi) {
@@ -439,353 +98,6 @@ describe('webContents module', () => {
})
})
describe('sendInputEvent(event)', () => {
beforeEach(async () => {
await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
})
it('can send keydown events', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('a')
expect(code).to.equal('KeyA')
expect(keyCode).to.equal(65)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
})
it('can send keydown events with modifiers', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Z')
expect(code).to.equal('KeyZ')
expect(keyCode).to.equal(90)
expect(shiftKey).to.be.true()
expect(ctrlKey).to.be.true()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
})
it('can send keydown events with special keys', (done) => {
ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Tab')
expect(code).to.equal('Tab')
expect(keyCode).to.equal(9)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.true()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] })
})
it('can send char events', (done) => {
ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('a')
expect(code).to.equal('KeyA')
expect(keyCode).to.equal(65)
expect(shiftKey).to.be.false()
expect(ctrlKey).to.be.false()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' })
})
it('can send char events with modifiers', (done) => {
ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
expect(key).to.equal('Z')
expect(code).to.equal('KeyZ')
expect(keyCode).to.equal(90)
expect(shiftKey).to.be.true()
expect(ctrlKey).to.be.true()
expect(altKey).to.be.false()
done()
})
w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' })
w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
})
})
it('supports inserting CSS', async () => {
w.loadURL('about:blank')
await w.webContents.insertCSS('body { background-repeat: round; }')
const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
expect(result).to.equal('round')
})
it('supports removing inserted CSS', async () => {
w.loadURL('about:blank')
const key = await w.webContents.insertCSS('body { background-repeat: round; }')
await w.webContents.removeInsertedCSS(key)
const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
expect(result).to.equal('repeat')
})
it('supports inspecting an element in the devtools', (done) => {
w.loadURL('about:blank')
w.webContents.once('devtools-opened', () => { done() })
w.webContents.inspectElement(10, 10)
})
describe('startDrag({file, icon})', () => {
it('throws errors for a missing file or a missing/empty icon', () => {
expect(() => {
w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') })
}).to.throw(`Must specify either 'file' or 'files' option`)
expect(() => {
w.webContents.startDrag({ file: __filename })
}).to.throw(`Must specify 'icon' option`)
if (process.platform === 'darwin') {
expect(() => {
w.webContents.startDrag({ file: __filename, icon: __filename })
}).to.throw(`Must specify non-empty 'icon' option`)
}
})
})
describe('focus()', () => {
describe('when the web contents is hidden', () => {
it('does not blur the focused window', (done) => {
ipcMain.once('answer', (event, parentFocused, childFocused) => {
expect(parentFocused).to.be.true()
expect(childFocused).to.be.false()
done()
})
w.show()
w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html'))
})
})
})
describe('getOSProcessId()', () => {
it('returns a valid procress id', async () => {
expect(w.webContents.getOSProcessId()).to.equal(0)
await w.loadURL('about:blank')
expect(w.webContents.getOSProcessId()).to.be.above(0)
})
})
describe('zoom api', () => {
const zoomScheme = remote.getGlobal('zoomScheme')
const hostZoomMap = {
host1: 0.3,
host2: 0.7,
host3: 0.2
}
before((done) => {
const protocol = session.defaultSession.protocol
protocol.registerStringProtocol(zoomScheme, (request, callback) => {
const response = `<script>
const {ipcRenderer, remote} = require('electron')
ipcRenderer.send('set-zoom', window.location.hostname)
ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
const { zoomLevel } = remote.getCurrentWebContents()
ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
})
</script>`
callback({ data: response, mimeType: 'text/html' })
}, (error) => done(error))
})
after((done) => {
const protocol = session.defaultSession.protocol
protocol.unregisterProtocol(zoomScheme, (error) => done(error))
})
// TODO(codebytere): remove in Electron v8.0.0
it('can set the correct zoom level (functions)', async () => {
try {
await w.loadURL('about:blank')
const zoomLevel = w.webContents.getZoomLevel()
expect(zoomLevel).to.eql(0.0)
w.webContents.setZoomLevel(0.5)
const newZoomLevel = w.webContents.getZoomLevel()
expect(newZoomLevel).to.eql(0.5)
} finally {
w.webContents.setZoomLevel(0)
}
})
it('can set the correct zoom level', async () => {
try {
await w.loadURL('about:blank')
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.eql(0.0)
w.webContents.zoomLevel = 0.5
const newZoomLevel = w.webContents.zoomLevel
expect(newZoomLevel).to.eql(0.5)
} finally {
w.webContents.zoomLevel = 0
}
})
it('can persist zoom level across navigation', (done) => {
let finalNavigation = false
ipcMain.on('set-zoom', (e, host) => {
const zoomLevel = hostZoomMap[host]
if (!finalNavigation) w.webContents.zoomLevel = zoomLevel
console.log()
e.sender.send(`${host}-zoom-set`)
})
ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
const expectedZoomLevel = hostZoomMap.host1
expect(zoomLevel).to.equal(expectedZoomLevel)
if (finalNavigation) {
done()
} else {
w.loadURL(`${zoomScheme}://host2`)
}
})
ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
const expectedZoomLevel = hostZoomMap.host2
expect(zoomLevel).to.equal(expectedZoomLevel)
finalNavigation = true
w.webContents.goBack()
})
w.loadURL(`${zoomScheme}://host1`)
})
it('can propagate zoom level across same session', (done) => {
const w2 = new BrowserWindow({
show: false
})
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(hostZoomMap.host3)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel1).to.equal(zoomLevel2)
w2.setClosable(true)
w2.close()
done()
})
w.webContents.on('did-finish-load', () => {
w.webContents.zoomLevel = hostZoomMap.host3
w2.loadURL(`${zoomScheme}://host3`)
})
w.loadURL(`${zoomScheme}://host3`)
})
it('cannot propagate zoom level across different session', (done) => {
const w2 = new BrowserWindow({
show: false,
webPreferences: {
partition: 'temp'
}
})
const protocol = w2.webContents.session.protocol
protocol.registerStringProtocol(zoomScheme, (request, callback) => {
callback('hello')
}, (error) => {
if (error) return done(error)
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(hostZoomMap.host3)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel2).to.equal(0)
expect(zoomLevel1).to.not.equal(zoomLevel2)
protocol.unregisterProtocol(zoomScheme, (error) => {
if (error) return done(error)
w2.setClosable(true)
w2.close()
done()
})
})
w.webContents.on('did-finish-load', () => {
w.webContents.zoomLevel = hostZoomMap.host3
w2.loadURL(`${zoomScheme}://host3`)
})
w.loadURL(`${zoomScheme}://host3`)
})
})
it('can persist when it contains iframe', (done) => {
const server = http.createServer((req, res) => {
setTimeout(() => {
res.end()
}, 200)
})
server.listen(0, '127.0.0.1', () => {
const url = 'http://127.0.0.1:' + server.address().port
const content = `<iframe src=${url}></iframe>`
w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
if (!isMainFrame) {
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.equal(2.0)
w.webContents.zoomLevel = 0
server.close()
done()
}
})
w.webContents.on('dom-ready', () => {
w.webContents.zoomLevel = 2.0
})
w.loadURL(`data:text/html,${content}`)
})
})
it('cannot propagate when used with webframe', (done) => {
let finalZoomLevel = 0
const w2 = new BrowserWindow({
show: false
})
w2.webContents.on('did-finish-load', () => {
const zoomLevel1 = w.webContents.zoomLevel
expect(zoomLevel1).to.equal(finalZoomLevel)
const zoomLevel2 = w2.webContents.zoomLevel
expect(zoomLevel2).to.equal(0)
expect(zoomLevel1).to.not.equal(zoomLevel2)
w2.setClosable(true)
w2.close()
done()
})
ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
w2.loadFile(path.join(fixtures, 'pages', 'c.html'))
finalZoomLevel = zoomLevel
})
w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html'))
})
it('cannot persist zoom level after navigation with webFrame', (done) => {
let initialNavigation = true
const source = `
const {ipcRenderer, webFrame} = require('electron')
webFrame.setZoomLevel(0.6)
ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
`
w.webContents.on('did-finish-load', () => {
if (initialNavigation) {
w.webContents.executeJavaScript(source)
} else {
const zoomLevel = w.webContents.zoomLevel
expect(zoomLevel).to.equal(0)
done()
}
})
ipcMain.once('zoom-level-set', (e, zoomLevel) => {
expect(zoomLevel).to.equal(0.6)
w.loadFile(path.join(fixtures, 'pages', 'd.html'))
initialNavigation = false
})
w.loadFile(path.join(fixtures, 'pages', 'c.html'))
})
})
describe('webrtc ip policy api', () => {
it('can set and get webrtc ip policies', () => {
const policies = [

View File

@@ -1,6 +1,6 @@
<html>
<head>
<link rel="stylesheet" href="http://127.0.0.1:8881/save_page/test.css">
<link rel="stylesheet" href="/save_page/test.css">
<script type="text/javascript">
window.ELECTRON_ENABLE_SECURITY_WARNINGS = true
</script>

View File

@@ -7,7 +7,6 @@
window.ajax = (url, options) => {
return new Promise((resolve, reject) => {
options.url = url
options.cache = false
options.success = (data, status, request) => {
resolve({data, status: request.status, headers: request.getAllResponseHeaders()})
}

View File

@@ -155,6 +155,19 @@ describe('node feature', () => {
})
})
})
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 remote 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', () => {

View File

@@ -8,7 +8,6 @@
},
"devDependencies": {
"basic-auth": "^2.0.1",
"bluebird": "^3.5.3",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"coffeescript": "^2.4.1",

View File

@@ -1,257 +0,0 @@
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const http = require('http')
const fs = require('fs')
const path = require('path')
const url = require('url')
const { remote } = require('electron')
const { BrowserWindow } = remote
const { closeWindow } = require('./window-helpers')
const { expect } = chai
chai.use(dirtyChai)
describe('security warnings', () => {
let server
let w = null
let useCsp = true
before((done) => {
// Create HTTP Server
server = http.createServer((request, response) => {
const uri = url.parse(request.url).pathname
let filename = path.join(__dirname, './fixtures/pages', uri)
fs.stat(filename, (error, stats) => {
if (error) {
response.writeHead(404, { 'Content-Type': 'text/plain' })
response.end()
return
}
if (stats.isDirectory()) {
filename += '/index.html'
}
fs.readFile(filename, 'binary', (err, file) => {
if (err) {
response.writeHead(404, { 'Content-Type': 'text/plain' })
response.end()
return
}
const cspHeaders = { 'Content-Security-Policy': `script-src 'self' 'unsafe-inline'` }
response.writeHead(200, useCsp ? cspHeaders : undefined)
response.write(file, 'binary')
response.end()
})
})
}).listen(8881, () => done())
})
after(() => {
// Close server
server.close()
server = null
})
afterEach(() => {
useCsp = true
return closeWindow(w).then(() => { w = null })
})
it('should warn about Node.js integration with remote content', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('Node.js Integration with Remote Content')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should not warn about Node.js integration with remote content from localhost', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.not.include('Node.js Integration with Remote Content')
if (message === 'loaded') {
done()
}
})
w.loadURL(`http://localhost:8881/base-page-security-onload-message.html`)
})
const generateSpecs = (description, webPreferences) => {
describe(description, () => {
it('should warn about disabled webSecurity', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
webSecurity: false,
...webPreferences
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('Disabled webSecurity')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should warn about insecure Content-Security-Policy', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
enableRemoteModule: false,
...webPreferences
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('Insecure Content-Security-Policy')
done()
})
useCsp = false
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should warn about allowRunningInsecureContent', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
allowRunningInsecureContent: true,
...webPreferences
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('allowRunningInsecureContent')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should warn about experimentalFeatures', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
experimentalFeatures: true,
...webPreferences
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('experimentalFeatures')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should warn about enableBlinkFeatures', (done) => {
w = new BrowserWindow({
show: false,
webPreferences: {
enableBlinkFeatures: ['my-cool-feature'],
...webPreferences
}
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('enableBlinkFeatures')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should warn about allowpopups', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('allowpopups')
done()
})
w.loadURL(`http://127.0.0.1:8881/webview-allowpopups.html`)
})
it('should warn about insecure resources', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('Insecure Resources')
done()
})
w.loadURL(`http://127.0.0.1:8881/insecure-resources.html`)
w.webContents.openDevTools()
})
it('should not warn about loading insecure-resources.html from localhost', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.not.include('insecure-resources.html')
done()
})
w.loadURL(`http://localhost:8881/insecure-resources.html`)
w.webContents.openDevTools()
})
it('should warn about enabled remote module with remote content', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.include('enableRemoteModule')
done()
})
w.loadURL(`http://127.0.0.1:8881/base-page-security.html`)
})
it('should not warn about enabled remote module with remote content from localhost', (done) => {
w = new BrowserWindow({
show: false,
webPreferences
})
w.webContents.once('console-message', (e, level, message) => {
expect(message).to.not.include('enableRemoteModule')
if (message === 'loaded') {
done()
}
})
w.loadURL(`http://localhost:8881/base-page-security-onload-message.html`)
})
})
}
generateSpecs('without sandbox', {})
generateSpecs('with sandbox', { sandbox: true })
})

View File

@@ -918,10 +918,17 @@ app.on('ready', () => {
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Item4', type: 'radio' }
])
appIcon.setTitle('title')
appIcon.setToolTip('This is my application.')
appIcon.setContextMenu(contextMenu)
appIcon.setImage('/path/to/new/icon')
appIcon.setPressedImage('/path/to/new/icon')
appIcon.popUpContextMenu(contextMenu, { x: 100, y: 100 })
appIcon.setContextMenu(contextMenu)
appIcon.setIgnoreDoubleClickEvents(true)
appIcon.on('click', (event, bounds) => {
console.log('click', event, bounds)
@@ -933,7 +940,12 @@ app.on('ready', () => {
appIcon.displayBalloon({
title: 'Hello World!',
content: 'This the the balloon content.'
content: 'This the the balloon content.',
iconType: 'error',
icon: 'path/to/icon',
respectQuietTime: true,
largeIcon: true,
noSound: true
})
})

View File

@@ -126,11 +126,6 @@ bl@^1.0.0:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
bluebird@^3.5.3:
version "3.5.4"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714"
integrity sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"

View File

@@ -141,6 +141,13 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/basic-auth@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/basic-auth/-/basic-auth-1.1.2.tgz#810fbded879f14327fc1d3413bdb92cfe9e24f73"
integrity sha512-NzkkcC+gkkILWaBi3+/z/3do6Ybk6TWeTqV5zCVXmG2KaBoT5YqlJvfqP44HCyDA+Cu58pp7uKAxy/G58se/TA==
dependencies:
"@types/node" "*"
"@types/body-parser@*":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c"
@@ -254,6 +261,14 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/send@^0.14.5":
version "0.14.5"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.14.5.tgz#653f7d25b93c3f7f51a8994addaf8a229de022a7"
integrity sha512-0mwoiK3DXXBu0GIfo+jBv4Wo5s1AcsxdpdwNUtflKm99VEMvmBPJ+/NBNRZy2R5JEYfWL/u4nAHuTUTA3wFecQ==
dependencies:
"@types/mime" "*"
"@types/node" "*"
"@types/serve-static@*":
version "1.13.2"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"