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)`
|
#### `contents.setWindowOpenHandler(handler)`
|
||||||
|
|
||||||
* `handler` Function<{action: 'deny'} | {action: 'allow', outlivesOpener?: boolean, overrideBrowserWindowOptions?: BrowserWindowConstructorOptions}>
|
* `handler` Function<[WindowOpenHandlerResponse](structures/window-open-handler-response.md)>
|
||||||
* `details` Object
|
* `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`.
|
* `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()`
|
* `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
|
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`.
|
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
|
Returns `WindowOpenHandlerResponse` - When set to `{ action: '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.
|
window. `{ action: 'allow' }` will allow the new window to be created.
|
||||||
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.
|
|
||||||
Returning an unrecognized value such as a null, undefined, or an object
|
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
|
without a recognized 'action' value will result in a console error and have
|
||||||
the same effect as returning `{action: 'deny'}`.
|
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
|
[`window.open()`](window-open.md) for more details and how to use this in
|
||||||
conjunction with `did-create-window`.
|
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)`
|
#### `contents.setAudioMuted(muted)`
|
||||||
|
|
||||||
* `muted` boolean
|
* `muted` boolean
|
||||||
|
|
|
@ -144,6 +144,7 @@ auto_filenames = {
|
||||||
"docs/api/structures/web-preferences.md",
|
"docs/api/structures/web-preferences.md",
|
||||||
"docs/api/structures/web-request-filter.md",
|
"docs/api/structures/web-request-filter.md",
|
||||||
"docs/api/structures/web-source.md",
|
"docs/api/structures/web-source.md",
|
||||||
|
"docs/api/structures/window-open-handler-response.md",
|
||||||
]
|
]
|
||||||
|
|
||||||
sandbox_bundle_deps = [
|
sandbox_bundle_deps = [
|
||||||
|
|
|
@ -435,14 +435,15 @@ WebContents.prototype.loadURL = function (url, options) {
|
||||||
return p;
|
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;
|
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 = {
|
const defaultResponse = {
|
||||||
browserWindowConstructorOptions: null,
|
browserWindowConstructorOptions: null,
|
||||||
outlivesOpener: false
|
outlivesOpener: false,
|
||||||
|
createWindow: undefined
|
||||||
};
|
};
|
||||||
if (!this._windowOpenHandler) {
|
if (!this._windowOpenHandler) {
|
||||||
return defaultResponse;
|
return defaultResponse;
|
||||||
|
@ -468,7 +469,8 @@ WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event,
|
||||||
} else if (response.action === 'allow') {
|
} else if (response.action === 'allow') {
|
||||||
return {
|
return {
|
||||||
browserWindowConstructorOptions: typeof response.overrideBrowserWindowOptions === 'object' ? response.overrideBrowserWindowOptions : null,
|
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 {
|
} else {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -655,13 +657,16 @@ WebContents.prototype._init = function () {
|
||||||
postData,
|
postData,
|
||||||
overrideBrowserWindowOptions: options || {},
|
overrideBrowserWindowOptions: options || {},
|
||||||
windowOpenArgs: details,
|
windowOpenArgs: details,
|
||||||
outlivesOpener: result.outlivesOpener
|
outlivesOpener: result.outlivesOpener,
|
||||||
|
createWindow: result.createWindow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
|
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
|
||||||
let windowOpenOutlivesOpenerOption: boolean = false;
|
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) => {
|
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 ? {
|
const postBody = postData ? {
|
||||||
data: postData,
|
data: postData,
|
||||||
|
@ -686,6 +691,7 @@ WebContents.prototype._init = function () {
|
||||||
|
|
||||||
windowOpenOutlivesOpenerOption = result.outlivesOpener;
|
windowOpenOutlivesOpenerOption = result.outlivesOpener;
|
||||||
windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
|
windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
|
||||||
|
createWindow = result.createWindow;
|
||||||
if (!event.defaultPrevented) {
|
if (!event.defaultPrevented) {
|
||||||
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
|
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
|
||||||
// Allow setting of backgroundColor as a webPreference even though
|
// Allow setting of backgroundColor as a webPreference even though
|
||||||
|
@ -715,6 +721,9 @@ WebContents.prototype._init = function () {
|
||||||
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
|
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
|
||||||
const overriddenOptions = windowOpenOverriddenOptions || undefined;
|
const overriddenOptions = windowOpenOverriddenOptions || undefined;
|
||||||
const outlivesOpener = windowOpenOutlivesOpenerOption;
|
const outlivesOpener = windowOpenOutlivesOpenerOption;
|
||||||
|
const windowOpenFunction = createWindow;
|
||||||
|
|
||||||
|
createWindow = undefined;
|
||||||
windowOpenOverriddenOptions = null;
|
windowOpenOverriddenOptions = null;
|
||||||
// false is the default
|
// false is the default
|
||||||
windowOpenOutlivesOpenerOption = false;
|
windowOpenOutlivesOpenerOption = false;
|
||||||
|
@ -737,7 +746,8 @@ WebContents.prototype._init = function () {
|
||||||
frameName,
|
frameName,
|
||||||
features: rawFeatures
|
features: rawFeatures
|
||||||
},
|
},
|
||||||
outlivesOpener
|
outlivesOpener,
|
||||||
|
createWindow: windowOpenFunction
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,16 @@ export type WindowOpenArgs = {
|
||||||
features: string,
|
features: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameNamesToWindow = new Map<string, BrowserWindow>();
|
const frameNamesToWindow = new Map<string, WebContents>();
|
||||||
const registerFrameNameToGuestWindow = (name: string, win: BrowserWindow) => frameNamesToWindow.set(name, win);
|
const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
|
||||||
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
|
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
|
* `openGuestWindow` is called to create and setup event handling for the new
|
||||||
* window.
|
* 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,
|
embedder: WebContents,
|
||||||
guest?: WebContents,
|
guest?: WebContents,
|
||||||
referrer: Referrer,
|
referrer: Referrer,
|
||||||
|
@ -34,7 +34,8 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
|
||||||
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
|
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
|
||||||
windowOpenArgs: WindowOpenArgs,
|
windowOpenArgs: WindowOpenArgs,
|
||||||
outlivesOpener: boolean,
|
outlivesOpener: boolean,
|
||||||
}): BrowserWindow | undefined {
|
createWindow?: Electron.CreateWindowFunction
|
||||||
|
}): void {
|
||||||
const { url, frameName, features } = windowOpenArgs;
|
const { url, frameName, features } = windowOpenArgs;
|
||||||
const { options: parsedOptions } = parseFeatures(features);
|
const { options: parsedOptions } = parseFeatures(features);
|
||||||
const browserWindowOptions = {
|
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
|
// To spec, subsequent window.open calls with the same frame name (`target` in
|
||||||
// spec parlance) will reuse the previous window.
|
// 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
|
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
|
||||||
const existingWindow = getGuestWindowByFrameName(frameName);
|
const existingWebContents = getGuestWebContentsByFrameName(frameName);
|
||||||
if (existingWindow) {
|
if (existingWebContents) {
|
||||||
if (existingWindow.isDestroyed() || existingWindow.webContents.isDestroyed()) {
|
if (existingWebContents.isDestroyed()) {
|
||||||
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
// FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
|
||||||
unregisterFrameName(frameName);
|
unregisterFrameName(frameName);
|
||||||
} else {
|
} else {
|
||||||
existingWindow.loadURL(url);
|
existingWebContents.loadURL(url);
|
||||||
return existingWindow;
|
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({
|
const window = new BrowserWindow({
|
||||||
webContents: guest,
|
webContents: guest,
|
||||||
...browserWindowOptions
|
...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 });
|
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 }: {
|
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
|
||||||
embedder: WebContents,
|
embedder: WebContents,
|
||||||
guest: BrowserWindow,
|
guest: WebContents,
|
||||||
frameName: string,
|
frameName: string,
|
||||||
outlivesOpener: boolean
|
outlivesOpener: boolean
|
||||||
}) {
|
}) {
|
||||||
const closedByEmbedder = function () {
|
const closedByEmbedder = function () {
|
||||||
guest.removeListener('closed', closedByUser);
|
guest.removeListener('destroyed', closedByUser);
|
||||||
guest.destroy();
|
guest.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,11 +134,11 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outl
|
||||||
if (!outlivesOpener) {
|
if (!outlivesOpener) {
|
||||||
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
|
||||||
}
|
}
|
||||||
guest.once('closed', closedByUser);
|
guest.once('destroyed', closedByUser);
|
||||||
|
|
||||||
if (frameName) {
|
if (frameName) {
|
||||||
registerFrameNameToGuestWindow(frameName, guest);
|
registerFrameNameToGuestWindow(frameName, guest);
|
||||||
guest.once('closed', function () {
|
guest.once('destroyed', function () {
|
||||||
unregisterFrameName(frameName);
|
unregisterFrameName(frameName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { BrowserWindow, screen } from 'electron';
|
import { BrowserWindow, screen } from 'electron';
|
||||||
import { expect, assert } from 'chai';
|
import { expect, assert } from 'chai';
|
||||||
import { HexColors, ScreenCapture } from './lib/screen-helpers';
|
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 { closeAllWindows } from './lib/window-helpers';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
|
||||||
describe('webContents.setWindowOpenHandler', () => {
|
describe('webContents.setWindowOpenHandler', () => {
|
||||||
|
describe('native window', () => {
|
||||||
let browserWindow: BrowserWindow;
|
let browserWindow: BrowserWindow;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
browserWindow = new BrowserWindow({ show: false });
|
browserWindow = new BrowserWindow({ show: false });
|
||||||
|
@ -214,3 +216,161 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||||
await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
|
await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('custom window', () => {
|
||||||
|
let browserWindow: BrowserWindow;
|
||||||
|
|
||||||
|
let server: http.Server;
|
||||||
|
let url: string;
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
url = (await listen(server)).url;
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
equal(other: WebContents): boolean;
|
||||||
browserWindowOptions: BrowserWindowConstructorOptions;
|
browserWindowOptions: BrowserWindowConstructorOptions;
|
||||||
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
|
_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;
|
_setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
|
||||||
_send(internal: boolean, channel: string, args: any): boolean;
|
_send(internal: boolean, channel: string, args: any): boolean;
|
||||||
_sendInternal(channel: string, ...args: any[]): void;
|
_sendInternal(channel: string, ...args: any[]): void;
|
||||||
|
@ -113,6 +113,8 @@ declare namespace Electron {
|
||||||
type?: 'backgroundPage' | 'window' | 'browserView' | 'remote' | 'webview' | 'offscreen';
|
type?: 'backgroundPage' | 'window' | 'browserView' | 'remote' | 'webview' | 'offscreen';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateWindowFunction = (options: BrowserWindowConstructorOptions) => WebContents;
|
||||||
|
|
||||||
interface Menu {
|
interface Menu {
|
||||||
_init(): void;
|
_init(): void;
|
||||||
_isCommandIdChecked(id: string): boolean;
|
_isCommandIdChecked(id: string): boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue