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 ELECTRON_TEST_RESULTS_DIR: junit
MOCHA_MULTI_REPORTERS: 'mocha-junit-reporter, tap' MOCHA_MULTI_REPORTERS: 'mocha-junit-reporter, tap'
MOCHA_REPORTER: mocha-multi-reporters MOCHA_REPORTER: mocha-multi-reporters
MOCHA_TIMEOUT: 120000
- task: PublishTestResults@2 - task: PublishTestResults@2
displayName: 'Publish Test Results' displayName: 'Publish Test Results'

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,10 +20,10 @@ const serializableTypes = [
Date, Date,
Error, Error,
RegExp, RegExp,
ArrayBuffer, ArrayBuffer
NativeImage
]; ];
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#Supported_types
export function isSerializableObject (value: any) { export function isSerializableObject (value: any) {
return value === null || ArrayBuffer.isView(value) || serializableTypes.some(type => value instanceof type); 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); return Object.fromEntries(targetEntries);
}; };
export function serialize (value: any): any { function serializeNativeImage (image: any) {
if (value instanceof NativeImage) { const representations = [];
const representations = []; const scaleFactors = image.getScaleFactors();
for (const scaleFactor of value.getScaleFactors()) {
const size = value.getSize(scaleFactor); // Use Buffer when there's only one representation for better perf.
const dataURL = value.toDataURL({ scaleFactor }); // 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 }); representations.push({ scaleFactor, size, dataURL });
} }
return { __ELECTRON_SERIALIZED_NativeImage__: true, representations }; }
} else if (value instanceof Buffer) { return { __ELECTRON_SERIALIZED_NativeImage__: true, representations };
return { __ELECTRON_SERIALIZED_Buffer__: true, data: value }; }
} else if (Array.isArray(value)) {
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); return value.map(serialize);
} else if (isSerializableObject(value)) { } else if (isSerializableObject(value)) {
return value; return value;
@@ -58,16 +95,7 @@ export function serialize (value: any): any {
export function deserialize (value: any): any { export function deserialize (value: any): any {
if (value && value.__ELECTRON_SERIALIZED_NativeImage__) { if (value && value.__ELECTRON_SERIALIZED_NativeImage__) {
const image = nativeImage.createEmpty(); return deserializeNativeImage(value);
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);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
return value.map(deserialize); return value.map(deserialize);
} else if (isSerializableObject(value)) { } else if (isSerializableObject(value)) {

View File

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

View File

@@ -125,7 +125,11 @@ class LocationProxy {
} }
private getGuestURL (): URL | null { 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 { try {
return new URL(urlString); return new URL(urlString);
} catch (e) { } catch (e) {

View File

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

View File

@@ -3,17 +3,8 @@ From: Jeremy Apthorp <jeremya@chromium.org>
Date: Wed, 10 Oct 2018 15:07:34 -0700 Date: Wed, 10 Oct 2018 15:07:34 -0700
Subject: command-ismediakey.patch Subject: command-ismediakey.patch
Override MediaKeysListener::IsMediaKeycode to also listen for Volume Up, Volume Down, Override MediaKeysListener::IsMediaKeycode and associated functions to also listen for
and Mute. We also need to patch out Chromium's usage of RemoteCommandCenterDelegate, as Volume Up, Volume Down, and Mute.
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.
Also apply electron/electron@0f67b1866a9f00b852370e721affa4efda623f3a Also apply electron/electron@0f67b1866a9f00b852370e721affa4efda623f3a
and electron/electron@d2368d2d3b3de9eec4cc32b6aaf035cc89921bf1 as and electron/electron@d2368d2d3b3de9eec4cc32b6aaf035cc89921bf1 as
@@ -95,3 +86,19 @@ index 85378bb565de617b1bd611d28c8714361747a357..36de4c0b0353be2418dacd388e92d7c3
return event; 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 { ELECTRON_DIR } = require('../../lib/utils');
const notesGenerator = require('./notes.js'); 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 runGit = async (args) => {
const response = await GitProcess.exec(args, ELECTRON_DIR); const response = await GitProcess.exec(args, ELECTRON_DIR);
@@ -60,7 +60,7 @@ const getAllBranches = async () => {
const getStabilizationBranches = async () => { const getStabilizationBranches = async () => {
return (await getAllBranches()) 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) => { const getPreviousStabilizationBranch = async (current) => {

View File

@@ -39,12 +39,6 @@ class GHKey {
this.repo = repo; this.repo = repo;
this.number = number; 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 { class Commit {
@@ -295,33 +289,9 @@ async function runRetryable (fn, maxRetries) {
if (lastError.status !== 404) throw lastError; 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 getPullRequest = async (ghKey) => {
const { number, owner, repo } = 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 }); const retryableFunc = () => octokit.pulls.get({ pull_number: number, owner, repo });
return checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT)); return checkCache(name, () => runRetryable(retryableFunc, MAX_FAIL_COUNT));
}; };
@@ -336,20 +306,10 @@ const getComments = async (ghKey) => {
const addRepoToPool = async (pool, repo, from, to) => { const addRepoToPool = async (pool, repo, from, to) => {
const commonAncestor = await getCommonAncestor(repo.dir, from, to); const commonAncestor = await getCommonAncestor(repo.dir, from, to);
// mark the old branch's commits as old news // add the commits
for (const oldHash of await getLocalCommitHashes(repo.dir, from)) { const oldHashes = await getLocalCommitHashes(repo.dir, from);
pool.processedHashes.add(oldHash); oldHashes.forEach(hash => { pool.processedHashes.add(hash); });
}
// get the new branch's commits and the pulls associated with them
const commits = await getLocalCommits(repo, commonAncestor, to); 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); pool.commits.push(...commits);
// add the pulls // add the pulls
@@ -357,7 +317,8 @@ const addRepoToPool = async (pool, repo, from, to) => {
let prKey; let prKey;
for (prKey of commit.prKeys.values()) { for (prKey of commit.prKeys.values()) {
const pull = await getPullRequest(prKey); 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; pool.pulls[prKey.number] = pull;
parsePullText(pull, commit); parsePullText(pull, commit);
} }

View File

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

View File

@@ -29,7 +29,9 @@ bool RegisteringMediaKeyForUntrustedClient(const ui::Accelerator& accelerator) {
if (base::mac::IsAtLeastOS10_14()) { if (base::mac::IsAtLeastOS10_14()) {
constexpr ui::KeyboardCode mediaKeys[] = { constexpr ui::KeyboardCode mediaKeys[] = {
ui::VKEY_MEDIA_PLAY_PAUSE, ui::VKEY_MEDIA_NEXT_TRACK, 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), if (std::find(std::begin(mediaKeys), std::end(mediaKeys),
accelerator.key_code()) != std::end(mediaKeys)) { accelerator.key_code()) != std::end(mediaKeys)) {
@@ -60,7 +62,7 @@ GlobalShortcut::~GlobalShortcut() {
void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) { void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
if (accelerator_callback_map_.find(accelerator) == if (accelerator_callback_map_.find(accelerator) ==
accelerator_callback_map_.end()) { 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. // notifies us with wrong accelerator.
NOTREACHED(); NOTREACHED();
return; return;

View File

@@ -756,6 +756,8 @@ void WebContents::BeforeUnloadFired(content::WebContents* tab,
*proceed_to_fire_unload = proceed; *proceed_to_fire_unload = proceed;
else else
*proceed_to_fire_unload = true; *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, void WebContents::SetContentsBounds(content::WebContents* source,
@@ -1546,6 +1548,9 @@ void WebContents::LoadURL(const GURL& url,
// Calling LoadURLWithParams() can trigger JS which destroys |this|. // Calling LoadURLWithParams() can trigger JS which destroys |this|.
auto weak_this = GetWeakPtr(); auto weak_this = GetWeakPtr();
// Required to make beforeunload handler work.
NotifyUserActivation();
params.transition_type = ui::PAGE_TRANSITION_TYPED; params.transition_type = ui::PAGE_TRANSITION_TYPED;
params.should_clear_history_list = true; params.should_clear_history_list = true;
params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE; params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE;
@@ -2675,6 +2680,15 @@ void WebContents::GrantOriginAccess(const GURL& url) {
url::Origin::Create(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( v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
const base::FilePath& file_path) { const base::FilePath& file_path) {
gin_helper::Promise<void> promise(isolate()); gin_helper::Promise<void> promise(isolate());

View File

@@ -367,6 +367,9 @@ class WebContents : public gin_helper::TrackableObject<WebContents>,
// the specified URL. // the specified URL.
void GrantOriginAccess(const GURL& 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); v8::Local<v8::Promise> TakeHeapSnapshot(const base::FilePath& file_path);
// Properties. // Properties.

View File

@@ -67,6 +67,7 @@
#include "ui/base/x/x11_util.h" #include "ui/base/x/x11_util.h"
#include "ui/base/x/x11_util_internal.h" #include "ui/base/x/x11_util_internal.h"
#include "ui/events/devices/x11/touch_factory_x11.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/gfx/x/x11_types.h"
#include "ui/gtk/gtk_ui.h" #include "ui/gtk/gtk_ui.h"
#include "ui/gtk/gtk_ui_delegate.h" #include "ui/gtk/gtk_ui_delegate.h"
@@ -210,10 +211,36 @@ int X11EmptyErrorHandler(Display* d, XErrorEvent* error) {
int X11EmptyIOErrorHandler(Display* d) { int X11EmptyIOErrorHandler(Display* d) {
return 0; 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 #endif
} // namespace } // 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 // static
ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr; ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr;
@@ -374,11 +401,19 @@ void ElectronBrowserMainParts::ToolkitInitialized() {
// In Aura/X11, Gtk-based LinuxUI implementation is used. // In Aura/X11, Gtk-based LinuxUI implementation is used.
gtk_ui_delegate_ = std::make_unique<ui::GtkUiDelegateX11>(gfx::GetXDisplay()); gtk_ui_delegate_ = std::make_unique<ui::GtkUiDelegateX11>(gfx::GetXDisplay());
ui::GtkUiDelegate::SetInstance(gtk_ui_delegate_.get()); ui::GtkUiDelegate::SetInstance(gtk_ui_delegate_.get());
views::LinuxUI::SetInstance(BuildGtkUi(ui::GtkUiDelegate::instance())); views::LinuxUI* linux_ui = BuildGtkUi(gtk_ui_delegate_.get());
#endif views::LinuxUI::SetInstance(linux_ui);
linux_ui->Initialize();
#if defined(USE_AURA) && defined(USE_X11) // Chromium does not respect GTK dark theme setting, but they may change
views::LinuxUI::instance()->Initialize(); // 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 #endif
#if defined(USE_AURA) #if defined(USE_AURA)

View File

@@ -59,6 +59,10 @@ class ViewsDelegate;
class ViewsDelegateMac; class ViewsDelegateMac;
#endif #endif
#if defined(USE_X11)
class DarkThemeObserver;
#endif
class ElectronBrowserMainParts : public content::BrowserMainParts { class ElectronBrowserMainParts : public content::BrowserMainParts {
public: public:
explicit ElectronBrowserMainParts(const content::MainFunctionParams& params); explicit ElectronBrowserMainParts(const content::MainFunctionParams& params);
@@ -129,6 +133,8 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
#if defined(USE_X11) #if defined(USE_X11)
std::unique_ptr<ui::GtkUiDelegate> gtk_ui_delegate_; 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 #endif
std::unique_ptr<views::LayoutProvider> layout_provider_; std::unique_ptr<views::LayoutProvider> layout_provider_;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import * as http from 'http';
import { AddressInfo } from 'net'; import { AddressInfo } from 'net';
import { app, BrowserWindow, BrowserView, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents } from 'electron/main'; 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 { ifit, ifdescribe } from './spec-helpers';
import { closeWindow, closeAllWindows } from './window-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 module', () => {
describe('BrowserWindow constructor', () => { describe('BrowserWindow constructor', () => {
it('allows passing void 0 as the webContents', async () => { it('allows passing void 0 as the webContents', async () => {
@@ -95,16 +99,11 @@ describe('BrowserWindow module', () => {
fs.unlinkSync(test); fs.unlinkSync(test);
expect(String(content)).to.equal('unload'); expect(String(content)).to.equal('unload');
}); });
it('should emit beforeunload handler', async () => { it('should emit beforeunload handler', async () => {
await w.loadFile(path.join(fixtures, 'api', 'beforeunload-false.html')); 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(); w.close();
await beforeunload; await emittedOnce(w.webContents, 'before-unload-fired');
}); });
describe('when invoked synchronously inside navigation observer', () => { describe('when invoked synchronously inside navigation observer', () => {
@@ -185,13 +184,11 @@ describe('BrowserWindow module', () => {
fs.unlinkSync(test); fs.unlinkSync(test);
expect(content).to.equal('close'); expect(content).to.equal('close');
}); });
it('should emit beforeunload event', async function () { 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')); await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true); w.webContents.executeJavaScript('window.close()', true);
const [e] = await emittedOnce(ipcMain, 'onbeforeunload'); await emittedOnce(w.webContents, 'before-unload-fired');
e.returnValue = null;
}); });
}); });
@@ -2629,32 +2626,31 @@ describe('BrowserWindow module', () => {
}); });
describe('beforeunload handler', function () { 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; let w: BrowserWindow = null as unknown as BrowserWindow;
beforeEach(() => { beforeEach(() => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
}); });
afterEach(() => {
ipcMain.removeAllListeners('onbeforeunload');
});
afterEach(closeAllWindows); afterEach(closeAllWindows);
it('returning undefined would not prevent close', (done) => {
w.once('closed', () => { done(); }); it('returning undefined would not prevent close', async () => {
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('returning false would prevent close', async () => { it('returning false would prevent close', async () => {
await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html')); await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-false.html'));
w.webContents.executeJavaScript('run()', true); w.close();
const [e] = await emittedOnce(ipcMain, 'onbeforeunload'); const [, proceed] = await emittedOnce(w.webContents, 'before-unload-fired');
e.returnValue = null; expect(proceed).to.equal(false);
}); });
it('returning empty string would prevent close', (done) => {
ipcMain.once('onbeforeunload', (e) => { e.returnValue = null; done(); }); it('returning empty string would prevent close', async () => {
w.loadFile(path.join(__dirname, 'fixtures', 'api', 'close-beforeunload-empty-string.html')); 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 () => { it('emits for each close attempt', async () => {
@@ -2663,46 +2659,16 @@ describe('BrowserWindow module', () => {
const destroyListener = () => { expect.fail('Close was not prevented'); }; const destroyListener = () => { expect.fail('Close was not prevented'); };
w.webContents.once('destroyed', destroyListener); w.webContents.once('destroyed', destroyListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true); await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
{ w.close();
const p = emittedOnce(ipcMain, 'onbeforeunload'); await emittedOnce(w.webContents, 'before-unload-fired');
w.close(); w.close();
const [e] = await p; await emittedOnce(w.webContents, 'before-unload-fired');
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;
}
w.webContents.removeListener('destroyed', destroyListener); w.webContents.removeListener('destroyed', destroyListener);
const p = emittedOnce(w.webContents, 'destroyed'); const wait = emittedOnce(w, 'closed');
w.close(); w.close();
await p; await wait;
}); });
it('emits for each reload attempt', async () => { it('emits for each reload attempt', async () => {
@@ -2711,19 +2677,14 @@ describe('BrowserWindow module', () => {
const navigationListener = () => { expect.fail('Reload was not prevented'); }; const navigationListener = () => { expect.fail('Reload was not prevented'); };
w.webContents.once('did-start-navigation', navigationListener); w.webContents.once('did-start-navigation', navigationListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true); await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
w.reload(); w.reload();
{ // Chromium does not emit 'before-unload-fired' on WebContents for
const [e] = await emittedOnce(ipcMain, 'onbeforeunload'); // navigations, so we have to use other ways to know if beforeunload
e.returnValue = null; // is fired.
} await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
w.reload(); w.reload();
{ await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
w.webContents.removeListener('did-start-navigation', navigationListener); w.webContents.removeListener('did-start-navigation', navigationListener);
w.reload(); w.reload();
@@ -2736,19 +2697,14 @@ describe('BrowserWindow module', () => {
const navigationListener = () => { expect.fail('Reload was not prevented'); }; const navigationListener = () => { expect.fail('Reload was not prevented'); };
w.webContents.once('did-start-navigation', navigationListener); w.webContents.once('did-start-navigation', navigationListener);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true); await w.webContents.executeJavaScript('installBeforeUnload(2)', true);
w.loadURL('about:blank'); w.loadURL('about:blank');
{ // Chromium does not emit 'before-unload-fired' on WebContents for
const [e] = await emittedOnce(ipcMain, 'onbeforeunload'); // navigations, so we have to use other ways to know if beforeunload
e.returnValue = null; // is fired.
} await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
await w.webContents.executeJavaScript('preventNextBeforeUnload()', true);
w.loadURL('about:blank'); w.loadURL('about:blank');
{ await emittedUntil(w.webContents, 'console-message', isBeforeUnload);
const [e] = await emittedOnce(ipcMain, 'onbeforeunload');
e.returnValue = null;
}
w.webContents.removeListener('did-start-navigation', navigationListener); w.webContents.removeListener('did-start-navigation', navigationListener);
w.loadURL('about:blank'); w.loadURL('about:blank');
@@ -4172,7 +4128,7 @@ describe('BrowserWindow module', () => {
window.postMessage({openedLocation}, '*') window.postMessage({openedLocation}, '*')
`); `);
const [, data] = await p; 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[0])).to.be.false('first unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second 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'); const fixturesPath = path.resolve(__dirname, 'fixtures');
describe('Menu module', function () { describe('Menu module', function () {
this.timeout(5000);
describe('Menu.buildFromTemplate', () => { describe('Menu.buildFromTemplate', () => {
it('should be able to attach extra fields', () => { it('should be able to attach extra fields', () => {
const menu = Menu.buildFromTemplate([ const menu = Menu.buildFromTemplate([
@@ -885,9 +884,14 @@ describe('Menu module', function () {
const appProcess = cp.spawn(process.execPath, [appPath]); const appProcess = cp.spawn(process.execPath, [appPath]);
let output = ''; let output = '';
appProcess.stdout.on('data', data => { output += data; }); await new Promise((resolve) => {
appProcess.stdout.on('data', data => {
await emittedOnce(appProcess, 'exit'); output += data;
if (data.indexOf('Window has') > -1) {
resolve();
}
});
});
expect(output).to.include('Window has no menu'); expect(output).to.include('Window has no menu');
}); });

View File

@@ -366,11 +366,11 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
const w = makeWindow(); const w = makeWindow();
const remotely = makeRemotely(w); const remotely = makeRemotely(w);
it('can serialize an empty nativeImage', async () => { it('can serialize an empty nativeImage from renderer to main', async () => {
const getEmptyImage = (img: NativeImage) => img.isEmpty(); const getImageEmpty = (img: NativeImage) => img.isEmpty();
w().webContents.once('remote-get-global', (event) => { w().webContents.once('remote-get-global', (event) => {
event.returnValue = getEmptyImage; event.returnValue = getImageEmpty;
}); });
await expect(remotely(() => { await expect(remotely(() => {
@@ -379,11 +379,23 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
})).to.eventually.be.true(); })).to.eventually.be.true();
}); });
it('can serialize a non-empty nativeImage', async () => { it('can serialize an empty nativeImage from main to renderer', async () => {
const getNonEmptyImage = (img: NativeImage) => img.getSize(); 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) => { w().webContents.once('remote-get-global', (event) => {
event.returnValue = getNonEmptyImage; event.returnValue = getImageSize;
}); });
await expect(remotely(() => { await expect(remotely(() => {
@@ -393,6 +405,18 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
})).to.eventually.deep.equal({ width: 2, height: 2 }); })).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 () => { it('can properly create a menu with an nativeImage icon in the renderer', async () => {
await expect(remotely(() => { await expect(remotely(() => {
const { remote, nativeImage } = require('electron'); const { remote, nativeImage } = require('electron');

View File

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

View File

@@ -1233,7 +1233,10 @@ describe('chromium features', () => {
w.loadURL(pdfSource); w.loadURL(pdfSource);
const [, contents] = await emittedOnce(app, 'web-contents-created'); const [, contents] = await emittedOnce(app, 'web-contents-created');
expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html'); 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 () => { 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')); w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'pdf-in-iframe.html'));
const [, contents] = await emittedOnce(app, 'web-contents-created'); const [, contents] = await emittedOnce(app, 'web-contents-created');
expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html'); 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 () => { it('loads a devtools extension', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension')); customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
const winningMessage = emittedOnce(ipcMain, 'winning');
const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } }); const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } });
await w.loadURL(url); await w.loadURL(url);
w.webContents.openDevTools(); w.webContents.openDevTools();
showLastDevToolsPanel(w); showLastDevToolsPanel(w);
await emittedOnce(ipcMain, 'winning'); await winningMessage;
}); });
}); });

View File

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

View File

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

View File

@@ -1,23 +1,14 @@
<html> <html>
<body> <body>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
function run() { // Only prevent unload on the first window close
// Only prevent unload on the first window close var unloadPrevented = false;
var unloadPrevented = false; window.onbeforeunload = function() {
window.onbeforeunload = function() { if (!unloadPrevented) {
setTimeout(function() { unloadPrevented = true;
require('electron').ipcRenderer.sendSync('onbeforeunload'); console.log('prevent')
}, 0); return false;
if (!unloadPrevented) {
unloadPrevented = true;
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> </script>
</body> </body>

View File

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

View File

@@ -37,7 +37,7 @@ protocol.registerSchemesAsPrivileged([
{ scheme: 'bar', privileges: { standard: true } } { scheme: 'bar', privileges: { standard: true } }
]); ]);
app.whenReady().then(() => { app.whenReady().then(async () => {
require('ts-node/register'); require('ts-node/register');
const argv = require('yargs') const argv = require('yargs')
@@ -68,53 +68,45 @@ app.whenReady().then(() => {
if (argv.grep) mocha.grep(argv.grep); if (argv.grep) mocha.grep(argv.grep);
if (argv.invert) mocha.invert(); if (argv.invert) mocha.invert();
// Read all test files. const filter = (file) => {
const walker = require('walkdir').walk(__dirname, { if (!/-spec\.[tj]s$/.test(file)) {
no_recurse: true 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;
const testFiles = [];
walker.on('file', (file) => {
if (/-spec\.[tj]s$/.test(file) &&
(!moduleMatch || moduleMatch.test(file))) {
testFiles.push(file);
} }
// 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, '..'); const cb = () => {
// Ensure the callback is called after runner is defined
walker.on('end', () => { process.nextTick(() => {
testFiles.sort(); process.exit(runner.failures);
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);
});
};
// Set up chai in the correct order // Set up chai in the correct order
const chai = require('chai'); const chai = require('chai');
chai.use(require('chai-as-promised')); chai.use(require('chai-as-promised'));
chai.use(require('dirty-chai')); 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 // Only prevent unload on the first window close
var unloadPrevented = false; var unloadPrevented = false;
window.onbeforeunload = function() { window.onbeforeunload = function() {
setTimeout(function() {
require('electron').ipcRenderer.sendSync('onbeforeunload');
}, 0);
if (!unloadPrevented) { if (!unloadPrevented) {
unloadPrevented = true; unloadPrevented = true;
return false; 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> <body>
<script src="jquery-2.0.3.min.js"></script> <script src="jquery-2.0.3.min.js"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
(function() { (async function() {
// Deprecated APIs are still supported and should be tested. // Deprecated APIs are still supported and should be tested.
process.throwDeprecation = false process.throwDeprecation = false
@@ -49,47 +49,45 @@
if (query.grep) mocha.grep(query.grep) if (query.grep) mocha.grep(query.grep)
if (query.invert) mocha.invert() if (query.invert) mocha.invert()
const files = query.files ? query.files.split(',') : undefined const filter = (file) => {
if (!/-spec\.js$/.test(file)) {
// Read all test files. return false
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)
} }
// 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', () => { const runner = mocha.run(() => {
testFiles.sort() // Ensure the callback is called after runner is defined
testFiles.forEach((file) => { setTimeout(() => {
if (!files || files.includes(path.relative(baseElectronDir, file))) { ipcRenderer.send('process.exit', runner.failures)
mocha.addFile(file) }, 0)
}
})
// 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)
})
}) })
})() })()
</script> </script>