Compare commits

...

21 Commits

Author SHA1 Message Date
Electron Bot
12ea1c0c41 Bump v10.0.0-beta.2 2020-06-01 08:32:38 -07:00
Shelley Vohr
9d2aa93581 fix: ensure nativeImage serialization main->renderer (#23794)
* refactor: use typeutils for nativeImage serialization (#23693)

* fix: ensure nativeImage serialization main->renderer
2020-05-28 12:07:33 -07:00
trop[bot]
cee9e6f0d0 fix: weakly reference MenuModel from MenuController (#23806)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2020-05-28 09:47:43 -07:00
trop[bot]
eb93acc463 fix: handle asynchronous URL loading in bw proxy (#23804)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2020-05-28 09:41:54 -07:00
trop[bot]
241e74c098 test: refactor how spec files are collected (#23811)
Co-authored-by: Aleksei Kuzmin <alkuzmin@microsoft.com>
2020-05-28 09:40:59 -07:00
trop[bot]
c0183d15af fix: volume key globalShortcut registration (#23823)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2020-05-28 08:57:25 -07:00
trop[bot]
4fe7c9ac24 fix: only bezel frameless windows (#23809)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2020-05-28 08:52:58 -04:00
Cheng Zhao
50efa847a5 Revert "fix: trigger activate event when app is activated via app switcher (#23771)" (#23819)
This reverts commit 7709e600c6.
2020-05-28 08:47:55 -04:00
trop[bot]
c2354d44ea fix: pass correct buffer length (#23798)
Co-authored-by: Cheng Zhao <zcbenz@gmail.com>
2020-05-27 14:09:30 -07:00
trop[bot]
7709e600c6 fix: trigger activate event when app is activated via app switcher (#23771)
When application is activated thru macOS app switcher (cmd+tab) the
App's activate event is note emitted. The reason is that
`applicationShouldHandleReopen:hasVisibleWindows:` is sent only when app
is activated via Dock. Using `applicationDidBecomeActive:` is handling
all cases properly.

Co-authored-by: Lukas Weber <luweber@microsoft.com>
2020-05-27 09:52:10 +09:00
trop[bot]
0962c1bd74 ci: deflake WOA tests (#23769)
Co-authored-by: John Kleinschmidt <jkleinsc@github.com>
2020-05-26 13:22:55 -04:00
trop[bot]
471f80521d test: use WebContents event to test beforeunload (#23766)
Co-authored-by: Cheng Zhao <zcbenz@gmail.com>
2020-05-26 10:53:23 -04:00
trop[bot]
1fb11e1e76 fix: trigger about panel for about role on on win (#23717)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2020-05-22 00:55:25 -07:00
trop[bot]
f8508b3c18 fix: read GTK dark theme setting on Linux (#23711)
Co-authored-by: Cheng Zhao <zcbenz@gmail.com>
2020-05-21 21:07:42 -04:00
Electron Bot
95e3853b77 Bump v10.0.0-beta.1 2020-05-21 14:28:15 -07:00
Samuel Attard
9de5ede1fb Revert "feat: look harder for a commit's pull request. (#23593)"
This reverts commit 2342aaffbd.
2020-05-21 14:27:04 -07:00
Electron Bot
05efbbcdd5 Revert "Bump v10.0.0-beta.1"
This reverts commit 2789f32efb.
2020-05-21 14:23:22 -07:00
Electron Bot
2789f32efb Bump v10.0.0-beta.1 2020-05-21 14:23:00 -07:00
Samuel Attard
4c8b884998 fix: support 10-x-y in the release notes generator (#23709) 2020-05-21 14:13:17 -07:00
Electron Bot
03ddd2d7af Revert "Bump v10.0.0-beta.1"
This reverts commit c141b1a906.
2020-05-21 13:33:35 -07:00
Electron Bot
c141b1a906 Bump v10.0.0-beta.1 2020-05-21 13:33:02 -07:00
43 changed files with 510 additions and 389 deletions

View File

@@ -1 +1 @@
10.0.0-nightly.20200521
10.0.0-beta.2

View File

@@ -71,7 +71,6 @@ steps:
ELECTRON_TEST_RESULTS_DIR: junit
MOCHA_MULTI_REPORTERS: 'mocha-junit-reporter, tap'
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_TIMEOUT: 120000
- task: PublishTestResults@2
displayName: 'Publish Test Results'

View File

@@ -69,6 +69,7 @@ a `type`.
The `role` property can have following values:
* `undo`
* `about` - Trigger a native about panel (custom message box on Window, which does not provide its own).
* `redo`
* `cut`
* `copy`
@@ -94,7 +95,6 @@ The `role` property can have following values:
The following additional roles are available on _macOS_:
* `appMenu` - Whole default "App" menu (About, Services, etc.)
* `about` - Map to the `orderFrontStandardAboutPanel` action.
* `hide` - Map to the `hide` action.
* `hideOthers` - Map to the `hideOtherApplications` action.
* `unhide` - Map to the `unhideAllApplications` action.

View File

@@ -907,10 +907,10 @@ Returns `String` - The URL of the current web page.
```javascript
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL('http://github.com')
let currentURL = win.webContents.getURL()
console.log(currentURL)
win.loadURL('http://github.com').then(() => {
const currentURL = win.webContents.getURL()
console.log(currentURL)
})
```
#### `contents.getTitle()`

View File

@@ -10,7 +10,8 @@ const roles = {
about: {
get label () {
return isLinux ? 'About' : `About ${app.name}`;
}
},
...(isWindows && { appMethod: 'showAboutPanel' })
},
close: {
label: isMac ? 'Close Window' : 'Close',

View File

@@ -4,12 +4,13 @@ import * as electron from 'electron';
import { EventEmitter } from 'events';
import objectsRegistry from './objects-registry';
import { ipcMainInternal } from '../ipc-main-internal';
import { isPromise, isSerializableObject } from '@electron/internal/common/type-utils';
import { isPromise, isSerializableObject, deserialize, serialize } from '@electron/internal/common/type-utils';
import { Size } from 'electron/main';
const v8Util = process.electronBinding('v8_util');
const eventBinding = process.electronBinding('event');
const features = process.electronBinding('features');
const { NativeImage } = process.electronBinding('native_image');
if (!features.isRemoteModuleEnabled()) {
throw new Error('remote module is disabled');
@@ -114,6 +115,9 @@ type MetaType = {
} | {
type: 'promise',
then: MetaType
} | {
type: 'nativeimage'
value: electron.NativeImage
}
// Convert a real value into meta data.
@@ -124,6 +128,8 @@ const valueToMeta = function (sender: electron.WebContents, contextId: string, v
// Recognize certain types of objects.
if (value instanceof Buffer) {
type = 'buffer';
} else if (value instanceof NativeImage) {
type = 'nativeimage';
} else if (Array.isArray(value)) {
type = 'array';
} else if (value instanceof Error) {
@@ -147,6 +153,8 @@ const valueToMeta = function (sender: electron.WebContents, contextId: string, v
type,
members: value.map((el: any) => valueToMeta(sender, contextId, el, optimizeSimpleObject))
};
} else if (type === 'nativeimage') {
return { type, value: serialize(value) };
} else if (type === 'object' || type === 'function') {
return {
type,
@@ -234,7 +242,10 @@ type MetaTypeFromRenderer = {
} | {
type: 'object',
name: string,
members: { name: string, value: MetaTypeFromRenderer }[]
members: {
name: string,
value: MetaTypeFromRenderer
}[]
} | {
type: 'function-with-return-value',
value: MetaTypeFromRenderer
@@ -245,7 +256,12 @@ type MetaTypeFromRenderer = {
length: number
} | {
type: 'nativeimage',
value: { size: Size, buffer: Buffer, scaleFactor: number, dataURL: string }[]
value: {
size: Size,
buffer: Buffer,
scaleFactor: number,
dataURL: string
}[]
}
const fakeConstructor = (constructor: Function, name: string) =>
@@ -263,15 +279,8 @@ const fakeConstructor = (constructor: Function, name: string) =>
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
const metaToValue = function (meta: MetaTypeFromRenderer): any {
switch (meta.type) {
case 'nativeimage': {
const image = electron.nativeImage.createEmpty();
for (const rep of meta.value) {
const { size, scaleFactor, dataURL } = rep;
const { width, height } = size;
image.addRepresentation({ dataURL, scaleFactor, width, height });
}
return image;
}
case 'nativeimage':
return deserialize(meta.value);
case 'value':
return meta.value;
case 'remote-object':

View File

@@ -20,10 +20,10 @@ const serializableTypes = [
Date,
Error,
RegExp,
ArrayBuffer,
NativeImage
ArrayBuffer
];
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#Supported_types
export function isSerializableObject (value: any) {
return value === null || ArrayBuffer.isView(value) || serializableTypes.some(type => value instanceof type);
}
@@ -34,18 +34,55 @@ const objectMap = function (source: Object, mapper: (value: any) => any) {
return Object.fromEntries(targetEntries);
};
export function serialize (value: any): any {
if (value instanceof NativeImage) {
const representations = [];
for (const scaleFactor of value.getScaleFactors()) {
const size = value.getSize(scaleFactor);
const dataURL = value.toDataURL({ scaleFactor });
function serializeNativeImage (image: any) {
const representations = [];
const scaleFactors = image.getScaleFactors();
// Use Buffer when there's only one representation for better perf.
// This avoids compressing to/from PNG where it's not necessary to
// ensure uniqueness of dataURLs (since there's only one).
if (scaleFactors.length === 1) {
const scaleFactor = scaleFactors[0];
const size = image.getSize(scaleFactor);
const buffer = image.toBitmap({ scaleFactor });
representations.push({ scaleFactor, size, buffer });
} else {
// Construct from dataURLs to ensure that they are not lost in creation.
for (const scaleFactor of scaleFactors) {
const size = image.getSize(scaleFactor);
const dataURL = image.toDataURL({ scaleFactor });
representations.push({ scaleFactor, size, dataURL });
}
return { __ELECTRON_SERIALIZED_NativeImage__: true, representations };
} else if (value instanceof Buffer) {
return { __ELECTRON_SERIALIZED_Buffer__: true, data: value };
} else if (Array.isArray(value)) {
}
return { __ELECTRON_SERIALIZED_NativeImage__: true, representations };
}
function deserializeNativeImage (value: any) {
const image = nativeImage.createEmpty();
// Use Buffer when there's only one representation for better perf.
// This avoids compressing to/from PNG where it's not necessary to
// ensure uniqueness of dataURLs (since there's only one).
if (value.representations.length === 1) {
const { buffer, size, scaleFactor } = value.representations[0];
const { width, height } = size;
image.addRepresentation({ buffer, scaleFactor, width, height });
} else {
// Construct from dataURLs to ensure that they are not lost in creation.
for (const rep of value.representations) {
const { dataURL, size, scaleFactor } = rep;
const { width, height } = size;
image.addRepresentation({ dataURL, scaleFactor, width, height });
}
}
return image;
}
export function serialize (value: any): any {
if (value instanceof NativeImage) {
return serializeNativeImage(value);
} if (Array.isArray(value)) {
return value.map(serialize);
} else if (isSerializableObject(value)) {
return value;
@@ -58,16 +95,7 @@ export function serialize (value: any): any {
export function deserialize (value: any): any {
if (value && value.__ELECTRON_SERIALIZED_NativeImage__) {
const image = nativeImage.createEmpty();
for (const rep of value.representations) {
const { size, scaleFactor, dataURL } = rep;
const { width, height } = size;
image.addRepresentation({ dataURL, scaleFactor, width, height });
}
return image;
} else if (value && value.__ELECTRON_SERIALIZED_Buffer__) {
const { buffer, byteOffset, byteLength } = value.data;
return Buffer.from(buffer, byteOffset, byteLength);
return deserializeNativeImage(value);
} else if (Array.isArray(value)) {
return value.map(deserialize);
} else if (isSerializableObject(value)) {

View File

@@ -5,7 +5,7 @@ const { hasSwitch } = process.electronBinding('command_line');
const { NativeImage } = process.electronBinding('native_image');
const { CallbacksRegistry } = require('@electron/internal/renderer/remote/callbacks-registry');
const { isPromise, isSerializableObject } = require('@electron/internal/common/type-utils');
const { isPromise, isSerializableObject, serialize, deserialize } = require('@electron/internal/common/type-utils');
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal');
const callbacksRegistry = new CallbacksRegistry();
@@ -37,14 +37,7 @@ function wrapArgs (args, visited = new Set()) {
}
if (value instanceof NativeImage) {
const images = [];
for (const scaleFactor of value.getScaleFactors()) {
const size = value.getSize(scaleFactor);
const buffer = value.toBitmap({ scaleFactor });
const dataURL = value.toDataURL({ scaleFactor });
images.push({ buffer, scaleFactor, size, dataURL });
}
return { type: 'nativeimage', value: images };
return { type: 'nativeimage', value: serialize(value) };
} else if (Array.isArray(value)) {
visited.add(value);
const meta = {
@@ -226,6 +219,7 @@ function metaToValue (meta) {
const types = {
value: () => meta.value,
array: () => meta.members.map((member) => metaToValue(member)),
nativeimage: () => deserialize(meta.value),
buffer: () => Buffer.from(meta.value.buffer, meta.value.byteOffset, meta.value.byteLength),
promise: () => Promise.resolve({ then: metaToValue(meta.then) }),
error: () => metaToError(meta),

View File

@@ -125,7 +125,11 @@ class LocationProxy {
}
private getGuestURL (): URL | null {
const urlString = this._invokeWebContentsMethodSync('getURL') as string;
const maybeURL = this._invokeWebContentsMethodSync('getURL') as string;
// When there's no previous frame the url will be blank, so accountfor that here
// to prevent url parsing errors on an empty string.
const urlString = maybeURL !== '' ? maybeURL : 'about:blank';
try {
return new URL(urlString);
} catch (e) {

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "10.0.0-nightly.20200521",
"version": "10.0.0-beta.2",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {

View File

@@ -3,17 +3,8 @@ From: Jeremy Apthorp <jeremya@chromium.org>
Date: Wed, 10 Oct 2018 15:07:34 -0700
Subject: command-ismediakey.patch
Override MediaKeysListener::IsMediaKeycode to also listen for Volume Up, Volume Down,
and Mute. We also need to patch out Chromium's usage of RemoteCommandCenterDelegate, as
it uses MPRemoteCommandCenter. MPRemoteCommandCenter makes it such that GlobalShortcuts
in Electron will not work as intended, because by design an app does not receive remote
control events until it begins playing audio. This means that a media shortcut would not kick
into effect until you, for example, began playing a YouTube video which sort of defeats the
purpose of GlobalShortcuts.
At the moment there is no upstream possibility for this; but perhaps Chromium may
consider some kind of switch, enabled by default, which would conditionally choose to avoid usage of
RemoteCommandCenterDelegate on macOS.
Override MediaKeysListener::IsMediaKeycode and associated functions to also listen for
Volume Up, Volume Down, and Mute.
Also apply electron/electron@0f67b1866a9f00b852370e721affa4efda623f3a
and electron/electron@d2368d2d3b3de9eec4cc32b6aaf035cc89921bf1 as
@@ -95,3 +86,19 @@ index 85378bb565de617b1bd611d28c8714361747a357..36de4c0b0353be2418dacd388e92d7c3
return event;
}
diff --git a/ui/base/accelerators/system_media_controls_media_keys_listener.cc b/ui/base/accelerators/system_media_controls_media_keys_listener.cc
index 9d6084ceaccfd071549e63e3015f55ef292312ec..3f6af8b1b49bf0f226e9336c222884b07bf69e55 100644
--- a/ui/base/accelerators/system_media_controls_media_keys_listener.cc
+++ b/ui/base/accelerators/system_media_controls_media_keys_listener.cc
@@ -65,6 +65,11 @@ bool SystemMediaControlsMediaKeysListener::StartWatchingMediaKey(
case VKEY_MEDIA_STOP:
service_->SetIsStopEnabled(true);
break;
+ case VKEY_VOLUME_DOWN:
+ case VKEY_VOLUME_UP:
+ case VKEY_VOLUME_MUTE:
+ // Do nothing.
+ break;
default:
NOTREACHED();
}

View File

@@ -8,7 +8,7 @@ const semver = require('semver');
const { ELECTRON_DIR } = require('../../lib/utils');
const notesGenerator = require('./notes.js');
const semverify = version => version.replace(/^origin\//, '').replace('x', '0').replace(/-/g, '.');
const semverify = version => version.replace(/^origin\//, '').replace(/[xy]/g, '0').replace(/-/g, '.');
const runGit = async (args) => {
const response = await GitProcess.exec(args, ELECTRON_DIR);
@@ -60,7 +60,7 @@ const getAllBranches = async () => {
const getStabilizationBranches = async () => {
return (await getAllBranches())
.filter(branch => /^origin\/\d+-\d+-x$/.test(branch));
.filter(branch => /^origin\/\d+-\d+-x$/.test(branch) || /^origin\/\d+-x-y$/.test(branch));
};
const getPreviousStabilizationBranch = async (current) => {

View File

@@ -39,12 +39,6 @@ class GHKey {
this.repo = repo;
this.number = number;
}
static NewFromPull (pull) {
const owner = pull.base.repo.owner.login;
const repo = pull.base.repo.name;
const number = pull.number;
return new GHKey(owner, repo, number);
}
}
class Commit {
@@ -295,33 +289,9 @@ async function runRetryable (fn, maxRetries) {
if (lastError.status !== 404) throw lastError;
}
const getPullCacheFilename = ghKey => `${ghKey.owner}-${ghKey.repo}-pull-${ghKey.number}`;
const getCommitPulls = async (owner, repo, hash) => {
const name = `${owner}-${repo}-commit-${hash}`;
const retryableFunc = () => octokit.repos.listPullRequestsAssociatedWithCommit({ owner, repo, commit_sha: hash });
const ret = await checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT));
// only merged pulls belong in release notes
if (ret && ret.data) {
ret.data = ret.data.filter(pull => pull.merged_at);
}
// cache the pulls
if (ret && ret.data) {
for (const pull of ret.data) {
const cachefile = getPullCacheFilename(GHKey.NewFromPull(pull));
const payload = { ...ret, data: pull };
await checkCache(cachefile, () => payload);
}
}
return ret;
};
const getPullRequest = async (ghKey) => {
const { number, owner, repo } = ghKey;
const name = getPullCacheFilename(ghKey);
const name = `${owner}-${repo}-pull-${number}`;
const retryableFunc = () => octokit.pulls.get({ pull_number: number, owner, repo });
return checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT));
};
@@ -336,20 +306,10 @@ const getComments = async (ghKey) => {
const addRepoToPool = async (pool, repo, from, to) => {
const commonAncestor = await getCommonAncestor(repo.dir, from, to);
// mark the old branch's commits as old news
for (const oldHash of await getLocalCommitHashes(repo.dir, from)) {
pool.processedHashes.add(oldHash);
}
// get the new branch's commits and the pulls associated with them
// add the commits
const oldHashes = await getLocalCommitHashes(repo.dir, from);
oldHashes.forEach(hash => { pool.processedHashes.add(hash); });
const commits = await getLocalCommits(repo, commonAncestor, to);
for (const commit of commits) {
const { owner, repo, hash } = commit;
for (const pull of (await getCommitPulls(owner, repo, hash)).data) {
commit.prKeys.add(GHKey.NewFromPull(pull));
}
}
pool.commits.push(...commits);
// add the pulls
@@ -357,7 +317,8 @@ const addRepoToPool = async (pool, repo, from, to) => {
let prKey;
for (prKey of commit.prKeys.values()) {
const pull = await getPullRequest(prKey);
if (!pull || !pull.data) continue; // couldn't get it
if (!pull || !pull.data) break; // couldn't get it
if (pool.pulls[prKey.number]) break; // already have it
pool.pulls[prKey.number] = pull;
parsePullText(pull, commit);
}

View File

@@ -266,6 +266,9 @@ void BrowserWindow::OnCloseButtonClicked(bool* prevent_default) {
// Already closed by renderer
return;
// Required to make beforeunload handler work.
api_web_contents_->NotifyUserActivation();
if (web_contents()->NeedToFireBeforeUnloadOrUnload())
web_contents()->DispatchBeforeUnload(false /* auto_cancel */);
else

View File

@@ -29,7 +29,9 @@ bool RegisteringMediaKeyForUntrustedClient(const ui::Accelerator& accelerator) {
if (base::mac::IsAtLeastOS10_14()) {
constexpr ui::KeyboardCode mediaKeys[] = {
ui::VKEY_MEDIA_PLAY_PAUSE, ui::VKEY_MEDIA_NEXT_TRACK,
ui::VKEY_MEDIA_PREV_TRACK, ui::VKEY_MEDIA_STOP};
ui::VKEY_MEDIA_PREV_TRACK, ui::VKEY_MEDIA_STOP,
ui::VKEY_VOLUME_UP, ui::VKEY_VOLUME_DOWN,
ui::VKEY_VOLUME_MUTE};
if (std::find(std::begin(mediaKeys), std::end(mediaKeys),
accelerator.key_code()) != std::end(mediaKeys)) {
@@ -60,7 +62,7 @@ GlobalShortcut::~GlobalShortcut() {
void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
if (accelerator_callback_map_.find(accelerator) ==
accelerator_callback_map_.end()) {
// This should never occur, because if it does, GlobalGlobalShortcutListener
// This should never occur, because if it does, GlobalShortcutListener
// notifies us with wrong accelerator.
NOTREACHED();
return;

View File

@@ -756,6 +756,8 @@ void WebContents::BeforeUnloadFired(content::WebContents* tab,
*proceed_to_fire_unload = proceed;
else
*proceed_to_fire_unload = true;
// Note that Chromium does not emit this for navigations.
Emit("before-unload-fired", proceed);
}
void WebContents::SetContentsBounds(content::WebContents* source,
@@ -1546,6 +1548,9 @@ void WebContents::LoadURL(const GURL& url,
// Calling LoadURLWithParams() can trigger JS which destroys |this|.
auto weak_this = GetWeakPtr();
// Required to make beforeunload handler work.
NotifyUserActivation();
params.transition_type = ui::PAGE_TRANSITION_TYPED;
params.should_clear_history_list = true;
params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE;
@@ -2675,6 +2680,15 @@ void WebContents::GrantOriginAccess(const GURL& url) {
url::Origin::Create(url));
}
void WebContents::NotifyUserActivation() {
auto* frame = web_contents()->GetMainFrame();
if (!frame)
return;
mojo::AssociatedRemote<mojom::ElectronRenderer> renderer;
frame->GetRemoteAssociatedInterfaces()->GetInterface(&renderer);
renderer->NotifyUserActivation();
}
v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
const base::FilePath& file_path) {
gin_helper::Promise<void> promise(isolate());

View File

@@ -367,6 +367,9 @@ class WebContents : public gin_helper::TrackableObject<WebContents>,
// the specified URL.
void GrantOriginAccess(const GURL& url);
// Notifies the web page that there is user interaction.
void NotifyUserActivation();
v8::Local<v8::Promise> TakeHeapSnapshot(const base::FilePath& file_path);
// Properties.

View File

@@ -67,6 +67,7 @@
#include "ui/base/x/x11_util.h"
#include "ui/base/x/x11_util_internal.h"
#include "ui/events/devices/x11/touch_factory_x11.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/x/x11_types.h"
#include "ui/gtk/gtk_ui.h"
#include "ui/gtk/gtk_ui_delegate.h"
@@ -210,10 +211,36 @@ int X11EmptyErrorHandler(Display* d, XErrorEvent* error) {
int X11EmptyIOErrorHandler(Display* d) {
return 0;
}
// GTK does not provide a way to check if current theme is dark, so we compare
// the text and background luminosity to get a result.
// This trick comes from FireFox.
void UpdateDarkThemeSetting() {
float bg = color_utils::GetRelativeLuminance(gtk::GetBgColor("GtkLabel"));
float fg = color_utils::GetRelativeLuminance(gtk::GetFgColor("GtkLabel"));
bool is_dark = fg > bg;
// Pass it to NativeUi theme, which is used by the nativeTheme module and most
// places in Electron.
ui::NativeTheme::GetInstanceForNativeUi()->set_use_dark_colors(is_dark);
// Pass it to Web Theme, to make "prefers-color-scheme" media query work.
ui::NativeTheme::GetInstanceForWeb()->set_use_dark_colors(is_dark);
}
#endif
} // namespace
#if defined(USE_X11)
class DarkThemeObserver : public ui::NativeThemeObserver {
public:
DarkThemeObserver() = default;
// ui::NativeThemeObserver:
void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override {
UpdateDarkThemeSetting();
}
};
#endif
// static
ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr;
@@ -374,11 +401,19 @@ void ElectronBrowserMainParts::ToolkitInitialized() {
// In Aura/X11, Gtk-based LinuxUI implementation is used.
gtk_ui_delegate_ = std::make_unique<ui::GtkUiDelegateX11>(gfx::GetXDisplay());
ui::GtkUiDelegate::SetInstance(gtk_ui_delegate_.get());
views::LinuxUI::SetInstance(BuildGtkUi(ui::GtkUiDelegate::instance()));
#endif
views::LinuxUI* linux_ui = BuildGtkUi(gtk_ui_delegate_.get());
views::LinuxUI::SetInstance(linux_ui);
linux_ui->Initialize();
#if defined(USE_AURA) && defined(USE_X11)
views::LinuxUI::instance()->Initialize();
// Chromium does not respect GTK dark theme setting, but they may change
// in future and this code might be no longer needed. Check the Chromium
// issue to keep updated:
// https://bugs.chromium.org/p/chromium/issues/detail?id=998903
UpdateDarkThemeSetting();
// Update the naitve theme when GTK theme changes. The GetNativeTheme
// here returns a NativeThemeGtk, which monitors GTK settings.
dark_theme_observer_.reset(new DarkThemeObserver);
linux_ui->GetNativeTheme(nullptr)->AddObserver(dark_theme_observer_.get());
#endif
#if defined(USE_AURA)

View File

@@ -59,6 +59,10 @@ class ViewsDelegate;
class ViewsDelegateMac;
#endif
#if defined(USE_X11)
class DarkThemeObserver;
#endif
class ElectronBrowserMainParts : public content::BrowserMainParts {
public:
explicit ElectronBrowserMainParts(const content::MainFunctionParams& params);
@@ -129,6 +133,8 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
#if defined(USE_X11)
std::unique_ptr<ui::GtkUiDelegate> gtk_ui_delegate_;
// Used to notify the native theme of changes to dark mode.
std::unique_ptr<DarkThemeObserver> dark_theme_observer_;
#endif
std::unique_ptr<views::LayoutProvider> layout_provider_;

View File

@@ -1454,25 +1454,27 @@ void NativeWindowMac::SetVibrancy(const std::string& type) {
[effect_view setState:NSVisualEffectStateActive];
// Make frameless Vibrant windows have rounded corners.
CGFloat radius = 5.0f; // default corner radius
CGFloat dimension = 2 * radius + 1;
NSSize size = NSMakeSize(dimension, dimension);
NSImage* maskImage = [NSImage imageWithSize:size
flipped:NO
drawingHandler:^BOOL(NSRect rect) {
NSBezierPath* bezierPath = [NSBezierPath
bezierPathWithRoundedRect:rect
xRadius:radius
yRadius:radius];
[[NSColor blackColor] set];
[bezierPath fill];
return YES;
}];
[maskImage setCapInsets:NSEdgeInsetsMake(radius, radius, radius, radius)];
[maskImage setResizingMode:NSImageResizingModeStretch];
if (!has_frame()) {
CGFloat radius = 5.0f; // default corner radius
CGFloat dimension = 2 * radius + 1;
NSSize size = NSMakeSize(dimension, dimension);
NSImage* maskImage = [NSImage imageWithSize:size
flipped:NO
drawingHandler:^BOOL(NSRect rect) {
NSBezierPath* bezierPath = [NSBezierPath
bezierPathWithRoundedRect:rect
xRadius:radius
yRadius:radius];
[[NSColor blackColor] set];
[bezierPath fill];
return YES;
}];
[maskImage setCapInsets:NSEdgeInsetsMake(radius, radius, radius, radius)];
[maskImage setResizingMode:NSImageResizingModeStretch];
[effect_view setMaskImage:maskImage];
[window_ setCornerMask:maskImage];
[effect_view setMaskImage:maskImage];
[window_ setCornerMask:maskImage];
}
[[window_ contentView] addSubview:effect_view
positioned:NSWindowBelow

View File

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

View File

@@ -10,6 +10,7 @@
#include "base/callback.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string16.h"
namespace electron {
@@ -24,15 +25,13 @@ class ElectronMenuModel;
// as it only maintains weak references.
@interface ElectronMenuController : NSObject <NSMenuDelegate> {
@protected
electron::ElectronMenuModel* model_; // weak
base::WeakPtr<electron::ElectronMenuModel> model_;
base::scoped_nsobject<NSMenu> menu_;
BOOL isMenuOpen_;
BOOL useDefaultAccelerator_;
base::OnceClosure closeCallback;
}
@property(nonatomic, assign) electron::ElectronMenuModel* model;
// Builds a NSMenu from the pre-built model (must not be nil). Changes made
// to the contents of the model after calling this will not be noticed.
- (id)initWithModel:(electron::ElectronMenuModel*)model
@@ -46,6 +45,9 @@ class ElectronMenuModel;
// Programmatically close the constructed menu.
- (void)cancel;
- (electron::ElectronMenuModel*)model;
- (void)setModel:(electron::ElectronMenuModel*)model;
// Access to the constructed menu if the complex initializer was used. If the
// default initializer was used, then this will create the menu on first call.
- (NSMenu*)menu;

View File

@@ -8,6 +8,7 @@
#include <utility>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
@@ -87,6 +88,44 @@ NSMenu* MakeEmptySubmenu() {
} // namespace
// This class stores a base::WeakPtr<electron::ElectronMenuModel> as an
// Objective-C object, which allows it to be stored in the representedObject
// field of an NSMenuItem.
@interface WeakPtrToElectronMenuModelAsNSObject : NSObject
+ (instancetype)weakPtrForModel:(electron::ElectronMenuModel*)model;
+ (electron::ElectronMenuModel*)getFrom:(id)instance;
- (instancetype)initWithModel:(electron::ElectronMenuModel*)model;
- (electron::ElectronMenuModel*)menuModel;
@end
@implementation WeakPtrToElectronMenuModelAsNSObject {
base::WeakPtr<electron::ElectronMenuModel> _model;
}
+ (instancetype)weakPtrForModel:(electron::ElectronMenuModel*)model {
return [[[WeakPtrToElectronMenuModelAsNSObject alloc] initWithModel:model]
autorelease];
}
+ (electron::ElectronMenuModel*)getFrom:(id)instance {
return
[base::mac::ObjCCastStrict<WeakPtrToElectronMenuModelAsNSObject>(instance)
menuModel];
}
- (instancetype)initWithModel:(electron::ElectronMenuModel*)model {
if ((self = [super init])) {
_model = model->GetWeakPtr();
}
return self;
}
- (electron::ElectronMenuModel*)menuModel {
return _model.get();
}
@end
// Menu item is located for ease of removing it from the parent owner
static base::scoped_nsobject<NSMenuItem> recentDocumentsMenuItem_;
@@ -95,12 +134,18 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
@implementation ElectronMenuController
@synthesize model = model_;
- (electron::ElectronMenuModel*)model {
return model_.get();
}
- (id)initWithModel:(electron::ElectronMenuModel*)model
useDefaultAccelerator:(BOOL)use {
- (void)setModel:(electron::ElectronMenuModel*)model {
model_ = model->GetWeakPtr();
}
- (instancetype)initWithModel:(electron::ElectronMenuModel*)model
useDefaultAccelerator:(BOOL)use {
if ((self = [super init])) {
model_ = model;
model_ = model->GetWeakPtr();
isMenuOpen_ = NO;
useDefaultAccelerator_ = use;
[self menu];
@@ -115,8 +160,7 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
// while its context menu is still open.
[self cancel];
model_ = nil;
model_ = nullptr;
[super dealloc];
}
@@ -137,7 +181,7 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
itemWithTitle:@"Electron"] submenu] itemWithTitle:openTitle] retain]);
}
model_ = model;
model_ = model->GetWeakPtr();
[menu_ removeAllItems];
const int count = model->GetItemCount();
@@ -153,7 +197,8 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
if (isMenuOpen_) {
[menu_ cancelTracking];
isMenuOpen_ = NO;
model_->MenuWillClose();
if (model_)
model_->MenuWillClose();
if (!closeCallback.is_null()) {
base::PostTask(FROM_HERE, {BrowserThread::UI}, std::move(closeCallback));
}
@@ -290,8 +335,8 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
// model. Setting the target to |self| allows this class to participate
// in validation of the menu items.
[item setTag:index];
NSValue* modelObject = [NSValue valueWithPointer:model];
[item setRepresentedObject:modelObject]; // Retains |modelObject|.
[item setRepresentedObject:[WeakPtrToElectronMenuModelAsNSObject
weakPtrForModel:model]];
ui::Accelerator accelerator;
if (model->GetAcceleratorAtWithParams(index, useDefaultAccelerator_,
&accelerator)) {
@@ -331,9 +376,8 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
return NO;
NSInteger modelIndex = [item tag];
electron::ElectronMenuModel* model =
static_cast<electron::ElectronMenuModel*>(
[[(id)item representedObject] pointerValue]);
electron::ElectronMenuModel* model = [WeakPtrToElectronMenuModelAsNSObject
getFrom:[(id)item representedObject]];
DCHECK(model);
if (model) {
BOOL checked = model->IsItemCheckedAt(modelIndex);
@@ -352,8 +396,7 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
- (void)itemSelected:(id)sender {
NSInteger modelIndex = [sender tag];
electron::ElectronMenuModel* model =
static_cast<electron::ElectronMenuModel*>(
[[sender representedObject] pointerValue]);
[WeakPtrToElectronMenuModelAsNSObject getFrom:[sender representedObject]];
DCHECK(model);
if (model) {
NSEvent* event = [NSApp currentEvent];
@@ -369,7 +412,7 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
menu_.reset([[NSMenu alloc] initWithTitle:@""]);
[menu_ setDelegate:self];
if (model_)
[self populateWithModel:model_];
[self populateWithModel:model_.get()];
return menu_.get();
}
@@ -379,7 +422,8 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
- (void)menuWillOpen:(NSMenu*)menu {
isMenuOpen_ = YES;
model_->MenuWillShow();
if (model_)
model_->MenuWillShow();
}
- (void)menuDidClose:(NSMenu*)menu {

View File

@@ -7,6 +7,7 @@
#include <map>
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "ui/base/models/simple_menu_model.h"
@@ -69,6 +70,10 @@ class ElectronMenuModel : public ui::SimpleMenuModel {
void MenuWillClose() override;
void MenuWillShow() override;
base::WeakPtr<ElectronMenuModel> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
using SimpleMenuModel::GetSubmenuModelAt;
ElectronMenuModel* GetSubmenuModelAt(int index);
@@ -80,6 +85,8 @@ class ElectronMenuModel : public ui::SimpleMenuModel {
std::map<int, base::string16> sublabels_; // command id -> sublabel
base::ObserverList<Observer> observers_;
base::WeakPtrFactory<ElectronMenuModel> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(ElectronMenuModel);
};

View File

@@ -22,6 +22,8 @@ interface ElectronRenderer {
string context_id,
int32 object_id);
NotifyUserActivation();
TakeHeapSnapshot(handle file) => (bool success);
};

View File

@@ -93,8 +93,8 @@ bool IsRunningInDesktopBridgeImpl() {
}
}
UINT32 length;
wchar_t packageFamilyName[PACKAGE_FAMILY_NAME_MAX_LENGTH + 1];
UINT32 length = PACKAGE_FAMILY_NAME_MAX_LENGTH;
wchar_t packageFamilyName[PACKAGE_FAMILY_NAME_MAX_LENGTH];
HANDLE proc = GetCurrentProcess();
LONG result =
(*get_package_family_namePtr)(proc, &length, packageFamilyName);

View File

@@ -236,6 +236,12 @@ void ElectronApiServiceImpl::DereferenceRemoteJSCallback(
}
#endif
void ElectronApiServiceImpl::NotifyUserActivation() {
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
if (frame)
frame->NotifyUserActivation();
}
void ElectronApiServiceImpl::TakeHeapSnapshot(
mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) {

View File

@@ -39,6 +39,7 @@ class ElectronApiServiceImpl : public mojom::ElectronRenderer,
void DereferenceRemoteJSCallback(const std::string& context_id,
int32_t object_id) override;
#endif
void NotifyUserActivation() override;
void TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) override;

View File

@@ -7,7 +7,7 @@ import * as http from 'http';
import { AddressInfo } from 'net';
import { app, BrowserWindow, BrowserView, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents } from 'electron/main';
import { emittedOnce } from './events-helpers';
import { emittedOnce, emittedUntil } from './events-helpers';
import { ifit, ifdescribe } from './spec-helpers';
import { closeWindow, closeAllWindows } from './window-helpers';
@@ -38,6 +38,10 @@ const expectBoundsEqual = (actual: any, expected: any) => {
}
};
const isBeforeUnload = (event: Event, level: number, message: string) => {
return (message === 'beforeunload');
};
describe('BrowserWindow module', () => {
describe('BrowserWindow constructor', () => {
it('allows passing void 0 as the webContents', async () => {
@@ -95,16 +99,11 @@ describe('BrowserWindow module', () => {
fs.unlinkSync(test);
expect(String(content)).to.equal('unload');
});
it('should emit beforeunload handler', async () => {
await w.loadFile(path.join(fixtures, 'api', 'beforeunload-false.html'));
const beforeunload = new Promise(resolve => {
ipcMain.once('onbeforeunload', (e) => {
e.returnValue = null;
resolve();
});
});
w.close();
await beforeunload;
await emittedOnce(w.webContents, 'before-unload-fired');
});
describe('when invoked synchronously inside navigation observer', () => {
@@ -185,13 +184,11 @@ describe('BrowserWindow module', () => {
fs.unlinkSync(test);
expect(content).to.equal('close');
});
it('should emit beforeunload event', async function () {
// TODO(nornagon): deflake this test.
this.retries(3);
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true);
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
w.webContents.executeJavaScript('window.close()', true);
await emittedOnce(w.webContents, 'before-unload-fired');
});
});
@@ -2629,32 +2626,31 @@ describe('BrowserWindow module', () => {
});
describe('beforeunload handler', function () {
// TODO(nornagon): I feel like these tests _oughtn't_ be flakey, but
// beforeunload is in general not reliable on the web, so i'm not going to
// worry about it too much for now.
this.retries(3);
let w: BrowserWindow = null as unknown as BrowserWindow;
beforeEach(() => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
});
afterEach(() => {
ipcMain.removeAllListeners('onbeforeunload');
});
afterEach(closeAllWindows);
it('returning undefined would not prevent close', (done) => {
w.once('closed', () => { done(); });
w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-undefined.html'));
it('returning undefined would not prevent close', async () => {
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-undefined.html'));
const wait = emittedOnce(w, 'closed');
w.close();
await wait;
});
it('returning false would prevent close', async () => {
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true);
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
w.close();
const [, proceed] = await emittedOnce(w.webContents, 'before-unload-fired');
expect(proceed).to.equal(false);
});
it('returning empty string would prevent close', (done) => {
ipcMain.once('onbeforeunload', (e) => { e.returnValue = null; done(); });
w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-empty-string.html'));
it('returning empty string would prevent close', async () => {
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-empty-string.html'));
w.close();
const [, proceed] = await emittedOnce(w.webContents, 'before-unload-fired');
expect(proceed).to.equal(false);
});
it('emits for each close attempt', async () => {
@@ -2663,46 +2659,16 @@ describe('BrowserWindow module', () => {
const destroyListener = () => { expect.fail('Close was not prevented'); };
w.webContents.once('destroyed', destroyListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
{
const p = emittedOnce(ipcMain, 'onbeforeunload');
w.close();
const [e] = await p;
e.returnValue = null;
}
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
// Hi future test refactorer! I don't know what event this timeout allows
// to occur, but without it, this test becomes flaky at this point and
// sometimes the window gets closed even though a `beforeunload` handler
// has been installed. I looked for events being emitted by the
// `webContents` during this timeout period and found nothing, so it
// might be some sort of internal timeout being applied by the content/
// layer, or blink?
//
// In any case, this incantation reduces flakiness. I'm going to add a
// summoning circle for good measure.
//
// 🕯 🕯
// 🕯 🕯
// 🕯 🕯
await new Promise(resolve => setTimeout(resolve, 1000));
// 🕯 🕯
// 🕯 🕯
// 🕯 🕯
{
const p = emittedOnce(ipcMain, 'onbeforeunload');
w.close();
const [e] = await p;
e.returnValue = null;
}
await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
w.close();
await emittedOnce(w.webContents, 'before-unload-fired');
w.close();
await emittedOnce(w.webContents, 'before-unload-fired');
w.webContents.removeListener('destroyed', destroyListener);
const p = emittedOnce(w.webContents, 'destroyed');
const wait = emittedOnce(w, 'closed');
w.close();
await p;
await wait;
});
it('emits for each reload attempt', async () => {
@@ -2711,19 +2677,14 @@ describe('BrowserWindow module', () => {
const navigationListener = () => { expect.fail('Reload was not prevented'); };
w.webContents.once('did-start-navigation', navigationListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
w.reload();
{
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
// Chromium does not emit 'before-unload-fired' on WebContents for
// navigations, so we have to use other ways to know if beforeunload
// is fired.
await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
w.reload();
{
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
w.webContents.removeListener('did-start-navigation', navigationListener);
w.reload();
@@ -2736,19 +2697,14 @@ describe('BrowserWindow module', () => {
const navigationListener = () => { expect.fail('Reload was not prevented'); };
w.webContents.once('did-start-navigation', navigationListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
w.loadURL('about:blank');
{
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
// Chromium does not emit 'before-unload-fired' on WebContents for
// navigations, so we have to use other ways to know if beforeunload
// is fired.
await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
w.loadURL('about:blank');
{
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
w.webContents.removeListener('did-start-navigation', navigationListener);
w.loadURL('about:blank');
@@ -4172,7 +4128,7 @@ describe('BrowserWindow module', () => {
window.postMessage({openedLocation}, '*')
`);
const [, data] = await p;
expect(data.pageContext.openedLocation).to.equal('');
expect(data.pageContext.openedLocation).to.equal('about:blank');
});
});

View File

@@ -38,4 +38,21 @@ ifdescribe(process.platform !== 'win32')('globalShortcut module', () => {
expect(globalShortcut.isRegistered(accelerators[0])).to.be.false('first unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second unregistered');
});
it('does not crash when registering media keys as global shortcuts', () => {
const accelerators = [
'VolumeUp',
'VolumeDown',
'VolumeMute',
'MediaNextTrack',
'MediaPreviousTrack',
'MediaStop', 'MediaPlayPause'
];
expect(() => {
globalShortcut.registerAll(accelerators, () => {});
}).to.not.throw();
globalShortcut.unregisterAll();
});
});

View File

@@ -10,7 +10,6 @@ import { closeWindow } from './window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
describe('Menu module', function () {
this.timeout(5000);
describe('Menu.buildFromTemplate', () => {
it('should be able to attach extra fields', () => {
const menu = Menu.buildFromTemplate([
@@ -885,9 +884,14 @@ describe('Menu module', function () {
const appProcess = cp.spawn(process.execPath, [appPath]);
let output = '';
appProcess.stdout.on('data', data => { output += data; });
await emittedOnce(appProcess, 'exit');
await new Promise((resolve) => {
appProcess.stdout.on('data', data => {
output += data;
if (data.indexOf('Window has') > -1) {
resolve();
}
});
});
expect(output).to.include('Window has no menu');
});

View File

@@ -366,11 +366,11 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
const w = makeWindow();
const remotely = makeRemotely(w);
it('can serialize an empty nativeImage', async () => {
const getEmptyImage = (img: NativeImage) => img.isEmpty();
it('can serialize an empty nativeImage from renderer to main', async () => {
const getImageEmpty = (img: NativeImage) => img.isEmpty();
w().webContents.once('remote-get-global', (event) => {
event.returnValue = getEmptyImage;
event.returnValue = getImageEmpty;
});
await expect(remotely(() => {
@@ -379,11 +379,23 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
})).to.eventually.be.true();
});
it('can serialize a non-empty nativeImage', async () => {
const getNonEmptyImage = (img: NativeImage) => img.getSize();
it('can serialize an empty nativeImage from main to renderer', async () => {
w().webContents.once('remote-get-global', (event) => {
const emptyImage = require('electron').nativeImage.createEmpty();
event.returnValue = emptyImage;
});
await expect(remotely(() => {
const image = require('electron').remote.getGlobal('someFunction');
return image.isEmpty();
})).to.eventually.be.true();
});
it('can serialize a non-empty nativeImage from renderer to main', async () => {
const getImageSize = (img: NativeImage) => img.getSize();
w().webContents.once('remote-get-global', (event) => {
event.returnValue = getNonEmptyImage;
event.returnValue = getImageSize;
});
await expect(remotely(() => {
@@ -393,6 +405,18 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
})).to.eventually.deep.equal({ width: 2, height: 2 });
});
it('can serialize a non-empty nativeImage from main to renderer', async () => {
w().webContents.once('remote-get-global', (event) => {
const nonEmptyImage = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVQYlWP8//8/AwMDEwMDAwMDAwAkBgMBBMzldwAAAABJRU5ErkJggg==');
event.returnValue = nonEmptyImage;
});
await expect(remotely(() => {
const image = require('electron').remote.getGlobal('someFunction');
return image.getSize();
})).to.eventually.deep.equal({ width: 2, height: 2 });
});
it('can properly create a menu with an nativeImage icon in the renderer', async () => {
await expect(remotely(() => {
const { remote, nativeImage } = require('electron');

View File

@@ -42,23 +42,22 @@ describe('webContents module', () => {
});
describe('will-prevent-unload event', function () {
// TODO(nornagon): de-flake this properly
this.retries(3);
afterEach(closeAllWindows);
it('does not emit if beforeunload returns undefined', (done) => {
it('does not emit if beforeunload returns undefined', async () => {
const w = new BrowserWindow({ show: false });
w.once('closed', () => done());
w.webContents.once('will-prevent-unload', () => {
expect.fail('should not have fired');
});
w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-undefined.html'));
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-undefined.html'));
const wait = emittedOnce(w, 'closed');
w.close();
await wait;
});
it('emits if beforeunload returns false', async () => {
const w = new BrowserWindow({ show: false });
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true);
w.close();
await emittedOnce(w.webContents, 'will-prevent-unload');
});
@@ -66,8 +65,9 @@ describe('webContents module', () => {
const w = new BrowserWindow({ show: false });
w.webContents.once('will-prevent-unload', event => event.preventDefault());
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true);
await emittedOnce(w, 'closed');
const wait = emittedOnce(w, 'closed');
w.close();
await wait;
});
});

View File

@@ -1233,7 +1233,10 @@ describe('chromium features', () => {
w.loadURL(pdfSource);
const [, contents] = await emittedOnce(app, 'web-contents-created');
expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html');
await emittedOnce(contents, 'did-finish-load');
await new Promise((resolve) => {
contents.on('did-finish-load', resolve);
contents.on('did-frame-finish-load', resolve);
});
});
it('opens when loading a pdf resource in a iframe', async () => {
@@ -1241,7 +1244,10 @@ describe('chromium features', () => {
w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'pdf-in-iframe.html'));
const [, contents] = await emittedOnce(app, 'web-contents-created');
expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html');
await emittedOnce(contents, 'did-finish-load');
await new Promise((resolve) => {
contents.on('did-finish-load', resolve);
contents.on('did-frame-finish-load', resolve);
});
});
});

View File

@@ -294,11 +294,12 @@ describe('chrome extensions', () => {
it('loads a devtools extension', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
const winningMessage = emittedOnce(ipcMain, 'winning');
const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } });
await w.loadURL(url);
w.webContents.openDevTools();
showLastDevToolsPanel(w);
await emittedOnce(ipcMain, 'winning');
await winningMessage;
});
});

View File

@@ -1,15 +1,14 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
function preventNextBeforeUnload() {
function installBeforeUnload(removeAfterNTimes) {
let count = 0
window.addEventListener('beforeunload', function handler(e) {
e.preventDefault();
e.returnValue = '';
window.removeEventListener('beforeunload', handler)
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload')
}, 0);
return false;
setTimeout(() => console.log('beforeunload'))
if (++count <= removeAfterNTimes) {
e.preventDefault();
e.returnValue = '';
}
})
}
</script>

View File

@@ -4,16 +4,11 @@
// Only prevent unload on the first window close
var unloadPrevented = false;
window.onbeforeunload = function() {
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload');
}, 0);
if (!unloadPrevented) {
unloadPrevented = true;
return '';
}
}
window.onload = () => window.close();
</script>
</body>
</html>

View File

@@ -1,23 +1,14 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
function run() {
// Only prevent unload on the first window close
var unloadPrevented = false;
window.onbeforeunload = function() {
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload');
}, 0);
if (!unloadPrevented) {
unloadPrevented = true;
return false;
}
// Only prevent unload on the first window close
var unloadPrevented = false;
window.onbeforeunload = function() {
if (!unloadPrevented) {
unloadPrevented = true;
console.log('prevent')
return false;
}
// unload events don't get run unless load events have run.
if (document.readyState === 'complete')
window.close()
else
window.onload = () => window.close()
}
</script>
</body>

View File

@@ -2,11 +2,7 @@
<body>
<script type="text/javascript" charset="utf-8">
window.onbeforeunload = function() {
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload');
}, 0);
}
window.onload = () => window.close();
</script>
</body>
</html>

View File

@@ -37,7 +37,7 @@ protocol.registerSchemesAsPrivileged([
{ scheme: 'bar', privileges: { standard: true } }
]);
app.whenReady().then(() => {
app.whenReady().then(async () => {
require('ts-node/register');
const argv = require('yargs')
@@ -68,53 +68,45 @@ app.whenReady().then(() => {
if (argv.grep) mocha.grep(argv.grep);
if (argv.invert) mocha.invert();
// Read all test files.
const walker = require('walkdir').walk(__dirname, {
no_recurse: true
});
// This allows you to run specific modules only:
// npm run test -match=menu
const moduleMatch = process.env.npm_config_match
? new RegExp(process.env.npm_config_match, 'g')
: null;
const testFiles = [];
walker.on('file', (file) => {
if (/-spec\.[tj]s$/.test(file) &&
(!moduleMatch || moduleMatch.test(file))) {
testFiles.push(file);
const filter = (file) => {
if (!/-spec\.[tj]s$/.test(file)) {
return false;
}
// This allows you to run specific modules only:
// npm run test -match=menu
const moduleMatch = process.env.npm_config_match
? new RegExp(process.env.npm_config_match, 'g')
: null;
if (moduleMatch && !moduleMatch.test(file)) {
return false;
}
const baseElectronDir = path.resolve(__dirname, '..');
if (argv.files && !argv.files.includes(path.relative(baseElectronDir, file))) {
return false;
}
return true;
};
const getFiles = require('../spec/static/get-files');
const testFiles = await getFiles(__dirname, { filter });
testFiles.sort().forEach((file) => {
mocha.addFile(file);
});
const baseElectronDir = path.resolve(__dirname, '..');
walker.on('end', () => {
testFiles.sort();
testFiles.forEach((file) => {
if (!argv.files || argv.files.includes(path.relative(baseElectronDir, file))) {
mocha.addFile(file);
}
const cb = () => {
// Ensure the callback is called after runner is defined
process.nextTick(() => {
process.exit(runner.failures);
});
const cb = () => {
// Ensure the callback is called after runner is defined
process.nextTick(() => {
process.exit(runner.failures);
});
};
};
// Set up chai in the correct order
const chai = require('chai');
chai.use(require('chai-as-promised'));
chai.use(require('dirty-chai'));
// Set up chai in the correct order
const chai = require('chai');
chai.use(require('chai-as-promised'));
chai.use(require('dirty-chai'));
const runner = mocha.run(cb);
});
const runner = mocha.run(cb);
});
function partition (xs, f) {
const trues = [];
const falses = [];
xs.forEach(x => (f(x) ? trues : falses).push(x));
return [trues, falses];
}

View File

@@ -4,9 +4,6 @@
// Only prevent unload on the first window close
var unloadPrevented = false;
window.onbeforeunload = function() {
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload');
}, 0);
if (!unloadPrevented) {
unloadPrevented = true;
return false;

15
spec/static/get-files.js Normal file
View File

@@ -0,0 +1,15 @@
async function getFiles (directoryPath, { filter = null } = {}) {
const files = [];
const walker = require('walkdir').walk(directoryPath, {
no_recurse: true
});
walker.on('file', (file) => {
if (!filter || filter(file)) {
files.push(file);
}
});
await new Promise((resolve) => walker.on('end', resolve));
return files;
}
module.exports = getFiles;

View File

@@ -1,7 +1,7 @@
<body>
<script src="jquery-2.0.3.min.js"></script>
<script type="text/javascript" charset="utf-8">
(function() {
(async function() {
// Deprecated APIs are still supported and should be tested.
process.throwDeprecation = false
@@ -49,47 +49,45 @@
if (query.grep) mocha.grep(query.grep)
if (query.invert) mocha.invert()
const files = query.files ? query.files.split(',') : undefined
// Read all test files.
const walker = require('walkdir').walk(path.dirname(__dirname), {
no_recurse: true
})
// This allows you to run specific modules only:
// npm run test -match=menu
const moduleMatch = process.env.npm_config_match
? new RegExp(process.env.npm_config_match, 'g')
: null
const testFiles = []
walker.on('file', (file) => {
if (/-spec\.js$/.test(file) && (!moduleMatch || moduleMatch.test(file))) {
testFiles.push(file)
const filter = (file) => {
if (!/-spec\.js$/.test(file)) {
return false
}
// This allows you to run specific modules only:
// npm run test -match=menu
const moduleMatch = process.env.npm_config_match
? new RegExp(process.env.npm_config_match, 'g')
: null
if (moduleMatch && !moduleMatch.test(file)) {
return false
}
const files = query.files ? query.files.split(',') : undefined
const baseElectronDir = path.resolve(__dirname, '..', '..')
if (files && !files.includes(path.relative(baseElectronDir, file))) {
return false
}
return true
}
const getFiles = require('./get-files')
const testFiles = await getFiles(path.dirname(__dirname), { filter })
testFiles.sort().forEach((file) => {
mocha.addFile(file)
})
const baseElectronDir = path.resolve(__dirname, '..', '..')
// Set up chai in the correct order
const chai = require('chai')
chai.use(require('chai-as-promised'))
chai.use(require('dirty-chai'))
walker.on('end', () => {
testFiles.sort()
testFiles.forEach((file) => {
if (!files || files.includes(path.relative(baseElectronDir, file))) {
mocha.addFile(file)
}
})
// Set up chai in the correct order
const chai = require('chai')
chai.use(require('chai-as-promised'))
chai.use(require('dirty-chai'))
const runner = mocha.run(() => {
// Ensure the callback is called after runner is defined
setTimeout(() => {
ipcRenderer.send('process.exit', runner.failures)
}, 0)
})
const runner = mocha.run(() => {
// Ensure the callback is called after runner is defined
setTimeout(() => {
ipcRenderer.send('process.exit', runner.failures)
}, 0)
})
})()
</script>