2023-05-02 21:44:34 +00:00
import { BrowserWindow , screen } from 'electron' ;
2020-11-10 17:06:03 +00:00
import { expect , assert } from 'chai' ;
2024-07-17 00:16:25 +00:00
import { HexColors , ScreenCapture , hasCapturableScreen } from './lib/screen-helpers' ;
2024-02-29 15:15:01 +00:00
import { ifit , listen } from './lib/spec-helpers' ;
2023-01-25 21:01:25 +00:00
import { closeAllWindows } from './lib/window-helpers' ;
2023-06-15 14:42:27 +00:00
import { once } from 'node:events' ;
2024-02-29 15:15:01 +00:00
import * as http from 'node:http' ;
2020-04-21 20:23:00 +00:00
2020-11-10 17:06:03 +00:00
describe ( 'webContents.setWindowOpenHandler' , ( ) = > {
2024-02-29 15:15:01 +00:00
describe ( 'native window' , ( ) = > {
let browserWindow : BrowserWindow ;
beforeEach ( async ( ) = > {
browserWindow = new BrowserWindow ( { show : false } ) ;
await browserWindow . loadURL ( 'about:blank' ) ;
2022-06-14 16:22:23 +00:00
} ) ;
2024-02-29 15:15:01 +00:00
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 ) ;
}
}
} ) ;
2022-06-14 16:22:23 +00:00
2024-02-29 15:15:01 +00:00
browserWindow . webContents . on ( 'did-create-window' , ( ) = > {
assert . fail ( 'did-create-window should not be called with an overridden window.open' ) ;
} ) ;
2022-06-14 16:22:23 +00:00
2024-02-29 15:15:01 +00:00
browserWindow . webContents . executeJavaScript ( "window.open('about:blank', '', 'show=no') && true" ) ;
2022-06-14 16:22:23 +00:00
browserWindow . webContents . setWindowOpenHandler ( ( ) = > {
2024-02-29 15:15:01 +00:00
throw error ;
2022-06-14 16:22:23 +00:00
} ) ;
} ) ;
2024-02-29 15:15:01 +00:00
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 ;
} ) ;
} ) ;
2022-06-14 16:22:23 +00:00
2024-02-29 15:15:01 +00:00
browserWindow . webContents . on ( 'did-create-window' , ( ) = > {
assert . fail ( 'did-create-window should not be called with an overridden window.open' ) ;
} ) ;
2022-06-14 16:22:23 +00:00
2024-02-29 15:15:01 +00:00
browserWindow . webContents . executeJavaScript ( "window.open('about:blank', '', 'show=no') && true" ) ;
2022-06-14 16:22:23 +00:00
2024-02-29 15:15:01 +00:00
await bad ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
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 ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
it ( 'is called when clicking on a target=_blank link' , async ( ) = > {
const denied = new Promise ( ( resolve ) = > {
browserWindow . webContents . setWindowOpenHandler ( ( ) = > {
setTimeout ( resolve ) ;
return { action : 'deny' } ;
} ) ;
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
browserWindow . webContents . on ( 'did-create-window' , ( ) = > {
assert . fail ( 'did-create-window should not be called with an overridden window.open' ) ;
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
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' ) ;
2021-04-06 08:04:14 +00:00
} ) ;
2024-02-29 15:15:01 +00:00
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 ;
2022-01-06 17:28:03 +00:00
} ) ;
2021-04-06 08:04:14 +00:00
2024-02-29 15:15:01 +00:00
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 : ''
} ) ;
2022-01-06 17:28:03 +00:00
} ) ;
2021-04-06 08:04:14 +00:00
2024-02-29 15:15:01 +00:00
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' )
} ]
} ) ;
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
it ( 'does fire window creation events if an override returns action: allow' , async ( ) = > {
browserWindow . webContents . setWindowOpenHandler ( ( ) = > ( { action : 'allow' } ) ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
setImmediate ( ( ) = > {
browserWindow . webContents . executeJavaScript ( "window.open('about:blank', '', 'show=no') && true" ) ;
2020-11-10 17:06:03 +00:00
} ) ;
2024-02-29 15:15:01 +00:00
await once ( browserWindow . webContents , 'did-create-window' ) ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
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' ) ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
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 ) ;
} ) ;
2020-11-10 17:06:03 +00:00
2024-07-17 00:16:25 +00:00
ifit ( hasCapturableScreen ( ) ) ( 'should not make child window background transparent' , async ( ) = > {
2024-02-29 15:15:01 +00:00
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;" ) ;
2024-07-17 00:16:25 +00:00
const screenCapture = new ScreenCapture ( display ) ;
2024-02-29 15:15:01 +00:00
// color-scheme is set to dark so background should not be white
await screenCapture . expectColorAtCenterDoesNotMatch ( HexColors . WHITE ) ;
} ) ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
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 } ` ) ;
}
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
url = ( await listen ( server ) ) . url ;
2022-01-06 17:28:03 +00:00
} ) ;
2024-02-29 15:15:01 +00:00
after ( ( ) = > {
server . close ( ) ;
2022-01-06 17:28:03 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
2024-02-29 15:15:01 +00:00
beforeEach ( async ( ) = > {
browserWindow = new BrowserWindow ( { show : false } ) ;
await browserWindow . loadURL ( ` ${ url } /index ` ) ;
2020-11-10 17:06:03 +00:00
} ) ;
2022-01-06 17:28:03 +00:00
2024-02-29 15:15:01 +00:00
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 ;
}
} ;
} ) ;
2022-01-06 17:28:03 +00:00
browserWindow . webContents . executeJavaScript ( "window.open('about:blank', '', 'show=no') && true" ) ;
2024-02-29 15:15:01 +00:00
await uncaughtExceptionEmitted ;
2022-01-06 17:28:03 +00:00
} ) ;
2024-02-29 15:15:01 +00:00
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" ) ;
} ) ;
2022-01-06 17:28:03 +00:00
2024-02-29 15:15:01 +00:00
expect ( childWindow . title ) . to . equal ( browserWindowTitle ) ;
} ) ;
2022-01-06 17:28:03 +00:00
2024-02-29 15:15:01 +00:00
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" ) ;
} ) ;
2023-02-20 11:30:57 +00:00
2024-02-29 15:15:01 +00:00
const size = childWindow . getSize ( ) ;
expect ( size [ 0 ] ) . to . equal ( 640 ) ;
expect ( size [ 1 ] ) . to . equal ( 480 ) ;
} ) ;
2022-01-06 17:28:03 +00:00
2024-02-29 15:15:01 +00:00
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 ` ) ;
} ) ;
2023-05-02 21:44:34 +00:00
2024-02-29 15:15:01 +00:00
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.' ) ;
} ) ;
2023-05-02 21:44:34 +00:00
} ) ;
2020-11-10 17:06:03 +00:00
} ) ;