mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
Compare commits
21 Commits
robo/add_w
...
v10.0.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ea1c0c41 | ||
|
|
9d2aa93581 | ||
|
|
cee9e6f0d0 | ||
|
|
eb93acc463 | ||
|
|
241e74c098 | ||
|
|
c0183d15af | ||
|
|
4fe7c9ac24 | ||
|
|
50efa847a5 | ||
|
|
c2354d44ea | ||
|
|
7709e600c6 | ||
|
|
0962c1bd74 | ||
|
|
471f80521d | ||
|
|
1fb11e1e76 | ||
|
|
f8508b3c18 | ||
|
|
95e3853b77 | ||
|
|
9de5ede1fb | ||
|
|
05efbbcdd5 | ||
|
|
2789f32efb | ||
|
|
4c8b884998 | ||
|
|
03ddd2d7af | ||
|
|
c141b1a906 |
@@ -1 +1 @@
|
||||
10.0.0-nightly.20200521
|
||||
10.0.0-beta.2
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -10,7 +10,8 @@ const roles = {
|
||||
about: {
|
||||
get label () {
|
||||
return isLinux ? 'About' : `About ${app.name}`;
|
||||
}
|
||||
},
|
||||
...(isWindows && { appMethod: 'showAboutPanel' })
|
||||
},
|
||||
close: {
|
||||
label: isMac ? 'Close Window' : 'Close',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ interface ElectronRenderer {
|
||||
string context_id,
|
||||
int32 object_id);
|
||||
|
||||
NotifyUserActivation();
|
||||
|
||||
TakeHeapSnapshot(handle file) => (bool success);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
3
spec/fixtures/api/beforeunload-false.html
vendored
3
spec/fixtures/api/beforeunload-false.html
vendored
@@ -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
15
spec/static/get-files.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user