feat: Allow creation of new window to be customizable. (#41432)
This commit is contained in:
parent
04df5ce492
commit
a0dad83ded
7 changed files with 441 additions and 220 deletions
7
docs/api/structures/window-open-handler-response.md
Normal file
7
docs/api/structures/window-open-handler-response.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# WindowOpenHandlerResponse Object
|
||||
|
||||
* `action` string - Can be `allow` or `deny`. Controls whether new window should be created.
|
||||
* `overrideBrowserWindowOptions` BrowserWindowConstructorOptions (optional) - Allows customization of the created window.
|
||||
* `outlivesOpener` boolean (optional) - By default, child windows are closed when their opener is closed. This can be
|
||||
changed by specifying `outlivesOpener: true`, in which case the opened window will not be closed when its opener is closed.
|
||||
* `createWindow` (options: BrowserWindowConstructorOptions) => WebContents (optional) - If specified, will be called instead of `new BrowserWindow` to create the new child window and event [`did-create-window`](../web-contents.md#event-did-create-window) will not be emitted. Constructed child window should use passed `options` object. This can be used for example to have the new window open as a BrowserView instead of in a separate window.
|
|
@ -1288,7 +1288,7 @@ Ignore application menu shortcuts while this web contents is focused.
|
|||
|
||||
#### `contents.setWindowOpenHandler(handler)`
|
||||
|
||||
* `handler` Function<{action: 'deny'} | {action: 'allow', outlivesOpener?: boolean, overrideBrowserWindowOptions?: BrowserWindowConstructorOptions}>
|
||||
* `handler` Function<[WindowOpenHandlerResponse](structures/window-open-handler-response.md)>
|
||||
* `details` Object
|
||||
* `url` string - The _resolved_ version of the URL passed to `window.open()`. e.g. opening a window with `window.open('foo')` will yield something like `https://the-origin/the/current/path/foo`.
|
||||
* `frameName` string - Name of the window provided in `window.open()`
|
||||
|
@ -1303,11 +1303,8 @@ Ignore application menu shortcuts while this web contents is focused.
|
|||
be set. If no post data is to be sent, the value will be `null`. Only defined
|
||||
when the window is being created by a form that set `target=_blank`.
|
||||
|
||||
Returns `{action: 'deny'} | {action: 'allow', outlivesOpener?: boolean, overrideBrowserWindowOptions?: BrowserWindowConstructorOptions}` - `deny` cancels the creation of the new
|
||||
window. `allow` will allow the new window to be created. Specifying `overrideBrowserWindowOptions` allows customization of the created window.
|
||||
By default, child windows are closed when their opener is closed. This can be
|
||||
changed by specifying `outlivesOpener: true`, in which case the opened window
|
||||
will not be closed when its opener is closed.
|
||||
Returns `WindowOpenHandlerResponse` - When set to `{ action: 'deny' }` cancels the creation of the new
|
||||
window. `{ action: 'allow' }` will allow the new window to be created.
|
||||
Returning an unrecognized value such as a null, undefined, or an object
|
||||
without a recognized 'action' value will result in a console error and have
|
||||
the same effect as returning `{action: 'deny'}`.
|
||||
|
@ -1318,6 +1315,26 @@ submitting a form with `<form target="_blank">`. See
|
|||
[`window.open()`](window-open.md) for more details and how to use this in
|
||||
conjunction with `did-create-window`.
|
||||
|
||||
An example showing how to customize the process of new `BrowserWindow` creation to be `BrowserView` attached to main window instead:
|
||||
|
||||
```js
|
||||
const { BrowserView, BrowserWindow } = require('electron')
|
||||
|
||||
const mainWindow = new BrowserWindow()
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: (options) => {
|
||||
const browserView = new BrowserView(options)
|
||||
mainWindow.addBrowserView(browserView)
|
||||
browserView.setBounds({ x: 0, y: 0, width: 640, height: 480 })
|
||||
return browserView.webContents
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### `contents.setAudioMuted(muted)`
|
||||
|
||||
* `muted` boolean
|
||||
|
|
|
@ -144,6 +144,7 @@ auto_filenames = {
|
|||
"docs/api/structures/web-preferences.md",
|
||||
"docs/api/structures/web-request-filter.md",
|
||||
"docs/api/structures/web-source.md",
|
||||
"docs/api/structures/window-open-handler-response.md",
|
||||
]
|
||||
|
||||
sandbox_bundle_deps = [
|
||||
|
|
|
@ -435,14 +435,15 @@ WebContents.prototype.loadURL = function (url, options) {
|
|||
return p;
|
||||
};
|
||||
|
||||
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => ({action: 'deny'} | {action: 'allow', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions, outlivesOpener?: boolean})) {
|
||||
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => Electron.WindowOpenHandlerResponse) {
|
||||
this._windowOpenHandler = handler;
|
||||
};
|
||||
|
||||
WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): {browserWindowConstructorOptions: BrowserWindowConstructorOptions | null, outlivesOpener: boolean} {
|
||||
WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): {browserWindowConstructorOptions: BrowserWindowConstructorOptions | null, outlivesOpener: boolean, createWindow?: Electron.CreateWindowFunction} {
|
||||
const defaultResponse = {
|
||||
browserWindowConstructorOptions: null,
|
||||
outlivesOpener: false
|
||||
outlivesOpener: false,
|
||||
createWindow: undefined
|
||||
};
|
||||
if (!this._windowOpenHandler) {
|
||||
return defaultResponse;
|
||||
|
@ -468,7 +469,8 @@ WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event,
|
|||
} else if (response.action === 'allow') {
|
||||
return {
|
||||
browserWindowConstructorOptions: typeof response.overrideBrowserWindowOptions === 'object' ? response.overrideBrowserWindowOptions : null,
|
||||
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
|
||||
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false,
|
||||
createWindow: typeof response.createWindow === 'function' ? response.createWindow : undefined
|
||||
};
|
||||
} else {
|
||||
event.preventDefault();
|
||||
|
@ -655,13 +657,16 @@ WebContents.prototype._init = function () {
|
|||
postData,
|
||||
overrideBrowserWindowOptions: options || {},
|
||||
windowOpenArgs: details,
|
||||
outlivesOpener: result.outlivesOpener
|
||||
outlivesOpener: result.outlivesOpener,
|
||||
createWindow: result.createWindow
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
|
||||
let windowOpenOutlivesOpenerOption: boolean = false;
|
||||
let createWindow: Electron.CreateWindowFunction | undefined;
|
||||
|
||||
this.on('-will-add-new-contents' as any, (event: Electron.Event, url: string, frameName: string, rawFeatures: string, disposition: Electron.HandlerDetails['disposition'], referrer: Electron.Referrer, postData: PostData) => {
|
||||
const postBody = postData ? {
|
||||
data: postData,
|
||||
|
@ -686,6 +691,7 @@ WebContents.prototype._init = function () {
|
|||
|
||||
windowOpenOutlivesOpenerOption = result.outlivesOpener;
|
||||
windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
|
||||
createWindow = result.createWindow;
|
||||
if (!event.defaultPrevented) {
|
||||
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
|
||||
// Allow setting of backgroundColor as a webPreference even though
|
||||
|
@ -715,6 +721,9 @@ WebContents.prototype._init = function () {
|
|||
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
|
||||
const overriddenOptions = windowOpenOverriddenOptions || undefined;
|
||||
const outlivesOpener = windowOpenOutlivesOpenerOption;
|
||||
const windowOpenFunction = createWindow;
|
||||
|
||||
createWindow = undefined;
|
||||
windowOpenOverriddenOptions = null;
|
||||
// false is the default
|
||||
windowOpenOutlivesOpenerOption = false;
|
||||
|
@ -737,7 +746,8 @@ WebContents.prototype._init = function () {
|
|||
frameName,
|
||||
features: rawFeatures
|
||||
},
|
||||
outlivesOpener
|
||||
outlivesOpener,
|
||||
createWindow: windowOpenFunction
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,16 +16,16 @@ export type WindowOpenArgs = {
|
|||
features: string,
|
||||
}
|
||||
|
||||
const frameNamesToWindow = new Map<string, BrowserWindow>();
|
||||
const registerFrameNameToGuestWindow = (name: string, win: BrowserWindow) => frameNamesToWindow.set(name, win);
|
||||
const frameNamesToWindow = new Map<string, WebContents>();
|
||||
const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
|
||||
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
|
||||
const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name);
|
||||
const getGuestWebContentsByFrameName = (name: string) => frameNamesToWindow.get(name);
|
||||
|
||||
/**
|
||||
* `openGuestWindow` is called to create and setup event handling for the new
|
||||
* window.
|
||||
*/
|
||||
export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
|
||||
export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener, createWindow }: {
|
||||
embedder: WebContents,
|
||||
guest?: WebContents,
|
||||
referrer: Referrer,
|
||||
|
@ -34,7 +34,8 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
|||
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
|
||||
windowOpenArgs: WindowOpenArgs,
|
||||
outlivesOpener: boolean,
|
||||
}): BrowserWindow | undefined {
|
||||
createWindow?: Electron.CreateWindowFunction
|
||||
}): void {
|
||||
const { url, frameName, features } = windowOpenArgs;
|
||||
const { options: parsedOptions } = parseFeatures(features);
|
||||
const browserWindowOptions = {
|
||||
|
@ -48,17 +49,42 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
|||
// To spec, subsequent window.open calls with the same frame name (`target` in
|
||||
// spec parlance) will reuse the previous window.
|
||||
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
|
||||
const existingWindow = getGuestWindowByFrameName(frameName);
|
||||
if (existingWindow) {
|
||||
if (existingWindow.isDestroyed() || existingWindow.webContents.isDestroyed()) {
|
||||
const existingWebContents = getGuestWebContentsByFrameName(frameName);
|
||||
if (existingWebContents) {
|
||||
if (existingWebContents.isDestroyed()) {
|
||||
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
||||
unregisterFrameName(frameName);
|
||||
} else {
|
||||
existingWindow.loadURL(url);
|
||||
return existingWindow;
|
||||
existingWebContents.loadURL(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (createWindow) {
|
||||
const webContents = createWindow({
|
||||
webContents: guest,
|
||||
...browserWindowOptions
|
||||
});
|
||||
|
||||
if (guest != null) {
|
||||
if (webContents !== guest) {
|
||||
throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
|
||||
}
|
||||
|
||||
webContents.loadURL(url, {
|
||||
httpReferrer: referrer,
|
||||
...(postData && {
|
||||
postData,
|
||||
extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
|
||||
})
|
||||
});
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest, outlivesOpener });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const window = new BrowserWindow({
|
||||
webContents: guest,
|
||||
...browserWindowOptions
|
||||
|
@ -77,11 +103,9 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
|||
});
|
||||
}
|
||||
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest: window, outlivesOpener });
|
||||
handleWindowLifecycleEvents({ embedder, frameName, guest: window.webContents, outlivesOpener });
|
||||
|
||||
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,12 +116,12 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
|||
*/
|
||||
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
|
||||
embedder: WebContents,
|
||||
guest: BrowserWindow,
|
||||
guest: WebContents,
|
||||
frameName: string,
|
||||
outlivesOpener: boolean
|
||||
}) {
|
||||
const closedByEmbedder = function () {
|
||||
guest.removeListener('closed', closedByUser);
|
||||
guest.removeListener('destroyed', closedByUser);
|
||||
guest.destroy();
|
||||
};
|
||||
|
||||
|
@ -110,11 +134,11 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
|
|||
if (!outlivesOpener) {
|
||||
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
||||
}
|
||||
guest.once('closed', closedByUser);
|
||||
guest.once('destroyed', closedByUser);
|
||||
|
||||
if (frameName) {
|
||||
registerFrameNameToGuestWindow(frameName, guest);
|
||||
guest.once('closed', function () {
|
||||
guest.once('destroyed', function () {
|
||||
unregisterFrameName(frameName);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,216 +1,376 @@
|
|||
import { BrowserWindow, screen } from 'electron';
|
||||
import { expect, assert } from 'chai';
|
||||
import { HexColors, ScreenCapture } from './lib/screen-helpers';
|
||||
import { ifit } from './lib/spec-helpers';
|
||||
import { ifit, listen } from './lib/spec-helpers';
|
||||
import { closeAllWindows } from './lib/window-helpers';
|
||||
import { once } from 'node:events';
|
||||
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
||||
import * as http from 'node:http';
|
||||
|
||||
describe('webContents.setWindowOpenHandler', () => {
|
||||
let browserWindow: BrowserWindow;
|
||||
beforeEach(async () => {
|
||||
browserWindow = new BrowserWindow({ show: false });
|
||||
await browserWindow.loadURL('about:blank');
|
||||
});
|
||||
describe('native window', () => {
|
||||
let browserWindow: BrowserWindow;
|
||||
beforeEach(async () => {
|
||||
browserWindow = new BrowserWindow({ show: false });
|
||||
await browserWindow.loadURL('about:blank');
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('does not fire window creation events if the handler callback throws an error', (done) => {
|
||||
const error = new Error('oh no');
|
||||
const listeners = process.listeners('uncaughtException');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.on('uncaughtException', (thrown) => {
|
||||
try {
|
||||
expect(thrown).to.equal(error);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
} finally {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
for (const listener of listeners) {
|
||||
process.on('uncaughtException', listener);
|
||||
it('does not fire window creation events if the handler callback throws an error', (done) => {
|
||||
const error = new Error('oh no');
|
||||
const listeners = process.listeners('uncaughtException');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
process.on('uncaughtException', (thrown) => {
|
||||
try {
|
||||
expect(thrown).to.equal(error);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
} finally {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
for (const listener of listeners) {
|
||||
process.on('uncaughtException', listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fire window creation events if the handler callback returns a bad result', async () => {
|
||||
const bad = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return [1, 2, 3] as any;
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
await bad;
|
||||
});
|
||||
|
||||
it('does not fire window creation events if an override returns action: deny', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when clicking on a target=_blank link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a target="_blank" href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when shift-clicking on a link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('fires handler with correct params', async () => {
|
||||
const testFrameName = 'test-frame-name';
|
||||
const testFeatures = 'top=10&left=10&something-unknown&show=no';
|
||||
const testUrl = 'app://does-not-exist/';
|
||||
const details = await new Promise<Electron.HandlerDetails>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler((details) => {
|
||||
setTimeout(() => resolve(details));
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
|
||||
});
|
||||
const { url, frameName, features, disposition, referrer } = details;
|
||||
expect(url).to.equal(testUrl);
|
||||
expect(frameName).to.equal(testFrameName);
|
||||
expect(features).to.equal(testFeatures);
|
||||
expect(disposition).to.equal('new-window');
|
||||
expect(referrer).to.deep.equal({
|
||||
policy: 'strict-origin-when-cross-origin',
|
||||
url: ''
|
||||
});
|
||||
});
|
||||
|
||||
it('includes post body', async () => {
|
||||
const details = await new Promise<Electron.HandlerDetails>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler((details) => {
|
||||
setTimeout(() => resolve(details));
|
||||
return { action: 'deny' };
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
|
||||
<form action="http://example.com" target="_blank" method="POST" id="form">
|
||||
<input name="key" value="value"></input>
|
||||
</form>
|
||||
<script>form.submit()</script>
|
||||
`)}`);
|
||||
});
|
||||
const { url, frameName, features, disposition, referrer, postBody } = details;
|
||||
expect(url).to.equal('http://example.com/');
|
||||
expect(frameName).to.equal('');
|
||||
expect(features).to.deep.equal('');
|
||||
expect(disposition).to.equal('foreground-tab');
|
||||
expect(referrer).to.deep.equal({
|
||||
policy: 'strict-origin-when-cross-origin',
|
||||
url: ''
|
||||
});
|
||||
expect(postBody).to.deep.equal({
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
data: [{
|
||||
type: 'rawData',
|
||||
bytes: Buffer.from('key=value')
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('does fire window creation events if an override returns action: allow', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
|
||||
|
||||
setImmediate(() => {
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
await once(browserWindow.webContents, 'did-create-window');
|
||||
it('does not fire window creation events if the handler callback returns a bad result', async () => {
|
||||
const bad = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return [1, 2, 3] as any;
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
await bad;
|
||||
});
|
||||
|
||||
it('does not fire window creation events if an override returns action: deny', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when clicking on a target=_blank link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a target="_blank" href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when shift-clicking on a link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('fires handler with correct params', async () => {
|
||||
const testFrameName = 'test-frame-name';
|
||||
const testFeatures = 'top=10&left=10&something-unknown&show=no';
|
||||
const testUrl = 'app://does-not-exist/';
|
||||
const details = await new Promise<Electron.HandlerDetails>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler((details) => {
|
||||
setTimeout(() => resolve(details));
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
|
||||
});
|
||||
const { url, frameName, features, disposition, referrer } = details;
|
||||
expect(url).to.equal(testUrl);
|
||||
expect(frameName).to.equal(testFrameName);
|
||||
expect(features).to.equal(testFeatures);
|
||||
expect(disposition).to.equal('new-window');
|
||||
expect(referrer).to.deep.equal({
|
||||
policy: 'strict-origin-when-cross-origin',
|
||||
url: ''
|
||||
});
|
||||
});
|
||||
|
||||
it('includes post body', async () => {
|
||||
const details = await new Promise<Electron.HandlerDetails>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler((details) => {
|
||||
setTimeout(() => resolve(details));
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
|
||||
<form action="http://example.com" target="_blank" method="POST" id="form">
|
||||
<input name="key" value="value"></input>
|
||||
</form>
|
||||
<script>form.submit()</script>
|
||||
`)}`);
|
||||
});
|
||||
const { url, frameName, features, disposition, referrer, postBody } = details;
|
||||
expect(url).to.equal('http://example.com/');
|
||||
expect(frameName).to.equal('');
|
||||
expect(features).to.deep.equal('');
|
||||
expect(disposition).to.equal('foreground-tab');
|
||||
expect(referrer).to.deep.equal({
|
||||
policy: 'strict-origin-when-cross-origin',
|
||||
url: ''
|
||||
});
|
||||
expect(postBody).to.deep.equal({
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
data: [{
|
||||
type: 'rawData',
|
||||
bytes: Buffer.from('key=value')
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('does fire window creation events if an override returns action: allow', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
|
||||
|
||||
setImmediate(() => {
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
});
|
||||
|
||||
await once(browserWindow.webContents, 'did-create-window');
|
||||
});
|
||||
|
||||
it('can change webPreferences of child windows', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
|
||||
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
|
||||
await childWindow.webContents.executeJavaScript("document.write('hello')");
|
||||
const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
|
||||
expect(size).to.equal('30px');
|
||||
});
|
||||
|
||||
it('does not hang parent window when denying window.open', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
||||
browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')");
|
||||
expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
|
||||
});
|
||||
|
||||
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
|
||||
ifit(process.platform === 'darwin' && process.arch === 'x64')('should not make child window background transparent', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window');
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
const display = screen.getPrimaryDisplay();
|
||||
childWindow.setBounds(display.bounds);
|
||||
await childWindow.webContents.executeJavaScript("const meta = document.createElement('meta'); meta.name = 'color-scheme'; meta.content = 'dark'; document.head.appendChild(meta); true;");
|
||||
await setTimeoutAsync(1000);
|
||||
const screenCapture = await ScreenCapture.createForDisplay(display);
|
||||
// color-scheme is set to dark so background should not be white
|
||||
await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
|
||||
});
|
||||
});
|
||||
|
||||
it('can change webPreferences of child windows', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
|
||||
describe('custom window', () => {
|
||||
let browserWindow: BrowserWindow;
|
||||
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
let server: http.Server;
|
||||
let url: string;
|
||||
|
||||
await childWindow.webContents.executeJavaScript("document.write('hello')");
|
||||
const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
|
||||
expect(size).to.equal('30px');
|
||||
});
|
||||
before(async () => {
|
||||
server = http.createServer((request, response) => {
|
||||
switch (request.url) {
|
||||
case '/index':
|
||||
response.statusCode = 200;
|
||||
response.end('<title>Index page</title>');
|
||||
break;
|
||||
case '/child':
|
||||
response.statusCode = 200;
|
||||
response.end('<title>Child page</title>');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported endpoint: ${request.url}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not hang parent window when denying window.open', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
||||
browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')");
|
||||
expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
|
||||
});
|
||||
url = (await listen(server)).url;
|
||||
});
|
||||
|
||||
// Linux and arm64 platforms (WOA and macOS) do not return any capture sources
|
||||
ifit(process.platform === 'darwin' && process.arch === 'x64')('should not make child window background transparent', async () => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
|
||||
const didCreateWindow = once(browserWindow.webContents, 'did-create-window');
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank') && true");
|
||||
const [childWindow] = await didCreateWindow;
|
||||
const display = screen.getPrimaryDisplay();
|
||||
childWindow.setBounds(display.bounds);
|
||||
await childWindow.webContents.executeJavaScript("const meta = document.createElement('meta'); meta.name = 'color-scheme'; meta.content = 'dark'; document.head.appendChild(meta); true;");
|
||||
await setTimeoutAsync(1000);
|
||||
const screenCapture = await ScreenCapture.createForDisplay(display);
|
||||
// color-scheme is set to dark so background should not be white
|
||||
await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
|
||||
after(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
browserWindow = new BrowserWindow({ show: false });
|
||||
await browserWindow.loadURL(`${url}/index`);
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('throws error when created window uses invalid webcontents', async () => {
|
||||
const listeners = process.listeners('uncaughtException');
|
||||
process.removeAllListeners('uncaughtException');
|
||||
const uncaughtExceptionEmitted = new Promise<void>((resolve, reject) => {
|
||||
process.on('uncaughtException', (thrown) => {
|
||||
try {
|
||||
expect(thrown.message).to.equal('Invalid webContents. Created window should be connected to webContents passed with options object.');
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
process.removeAllListeners('uncaughtException');
|
||||
listeners.forEach((listener) => process.on('uncaughtException', listener));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: () => {
|
||||
const childWindow = new BrowserWindow({ title: 'New window' });
|
||||
return childWindow.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
|
||||
await uncaughtExceptionEmitted;
|
||||
});
|
||||
|
||||
it('spawns browser window when createWindow is provided', async () => {
|
||||
const browserWindowTitle = 'Child browser window';
|
||||
|
||||
const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: (options) => {
|
||||
const childWindow = new BrowserWindow({ ...options, title: browserWindowTitle });
|
||||
resolve(childWindow);
|
||||
return childWindow.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
});
|
||||
|
||||
expect(childWindow.title).to.equal(browserWindowTitle);
|
||||
});
|
||||
|
||||
it('spawns browser window with overriden options', async () => {
|
||||
const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
width: 640,
|
||||
height: 480
|
||||
},
|
||||
createWindow: (options) => {
|
||||
expect(options.width).to.equal(640);
|
||||
expect(options.height).to.equal(480);
|
||||
const childWindow = new BrowserWindow(options);
|
||||
resolve(childWindow);
|
||||
return childWindow.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
|
||||
});
|
||||
|
||||
const size = childWindow.getSize();
|
||||
expect(size[0]).to.equal(640);
|
||||
expect(size[1]).to.equal(480);
|
||||
});
|
||||
|
||||
it('spawns browser window with access to opener property', async () => {
|
||||
const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: (options) => {
|
||||
const childWindow = new BrowserWindow(options);
|
||||
resolve(childWindow);
|
||||
return childWindow.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
browserWindow.webContents.executeJavaScript(`window.open('${url}/child', '', 'show=no') && true`);
|
||||
});
|
||||
|
||||
await once(childWindow.webContents, 'ready-to-show');
|
||||
const childWindowOpenerTitle = await childWindow.webContents.executeJavaScript('window.opener.document.title');
|
||||
expect(childWindowOpenerTitle).to.equal(browserWindow.title);
|
||||
});
|
||||
|
||||
it('spawns browser window without access to opener property because of noopener attribute ', async () => {
|
||||
const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
return {
|
||||
action: 'allow',
|
||||
createWindow: (options) => {
|
||||
const childWindow = new BrowserWindow(options);
|
||||
resolve(childWindow);
|
||||
return childWindow.webContents;
|
||||
}
|
||||
};
|
||||
});
|
||||
browserWindow.webContents.executeJavaScript(`window.open('${url}/child', '', 'noopener,show=no') && true`);
|
||||
});
|
||||
|
||||
await once(childWindow.webContents, 'ready-to-show');
|
||||
await expect(childWindow.webContents.executeJavaScript('window.opener.document.title')).to.be.rejectedWith('Script failed to execute, this normally means an error was thrown. Check the renderer console for the error.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
4
typings/internal-electron.d.ts
vendored
4
typings/internal-electron.d.ts
vendored
|
@ -77,7 +77,7 @@ declare namespace Electron {
|
|||
equal(other: WebContents): boolean;
|
||||
browserWindowOptions: BrowserWindowConstructorOptions;
|
||||
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
|
||||
_callWindowOpenHandler(event: any, details: Electron.HandlerDetails): {browserWindowConstructorOptions: Electron.BrowserWindowConstructorOptions | null, outlivesOpener: boolean};
|
||||
_callWindowOpenHandler(event: any, details: Electron.HandlerDetails): {browserWindowConstructorOptions: Electron.BrowserWindowConstructorOptions | null, outlivesOpener: boolean, createWindow?: Electron.CreateWindowFunction};
|
||||
_setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
|
||||
_send(internal: boolean, channel: string, args: any): boolean;
|
||||
_sendInternal(channel: string, ...args: any[]): void;
|
||||
|
@ -113,6 +113,8 @@ declare namespace Electron {
|
|||
type?: 'backgroundPage' | 'window' | 'browserView' | 'remote' | 'webview' | 'offscreen';
|
||||
}
|
||||
|
||||
type CreateWindowFunction = (options: BrowserWindowConstructorOptions) => WebContents;
|
||||
|
||||
interface Menu {
|
||||
_init(): void;
|
||||
_isCommandIdChecked(id: string): boolean;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue