'use strict' const assert = require('assert') const chai = require('chai') const dirtyChai = require('dirty-chai') const fs = require('fs') const path = require('path') const os = require('os') const qs = require('querystring') const http = require('http') const {closeWindow} = require('./window-helpers') const {emittedOnce} = require('./events-helpers') const {ipcRenderer, remote, screen} = require('electron') const {app, ipcMain, BrowserWindow, BrowserView, protocol, session, webContents} = remote const features = process.atomBinding('features') const {expect} = chai const isCI = remote.getGlobal('isCi') const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled') chai.use(dirtyChai) describe('BrowserWindow module', () => { const fixtures = path.resolve(__dirname, 'fixtures') let w = null let ws = null let server let postData const defaultOptions = { show: false, width: 400, height: 400, webPreferences: { backgroundThrottling: false } } const openTheWindow = async (options = defaultOptions) => { // The `afterEach` hook isn't called if a test fails, // we should make sure that the window is closed ourselves. await closeTheWindow() w = new BrowserWindow(options) return w } const closeTheWindow = function () { return closeWindow(w).then(() => { w = null }) } before((done) => { const filePath = path.join(fixtures, 'pages', 'a.html') const fileStats = fs.statSync(filePath) postData = [ { type: 'rawData', bytes: Buffer.from('username=test&file=') }, { type: 'file', filePath: filePath, offset: 0, length: fileStats.size, modificationTime: fileStats.mtime.getTime() / 1000 } ] server = http.createServer((req, res) => { function respond () { if (req.method === 'POST') { let body = '' req.on('data', (data) => { if (data) body += data }) req.on('end', () => { let parsedData = qs.parse(body) fs.readFile(filePath, (err, data) => { if (err) return if (parsedData.username === 'test' && parsedData.file === data.toString()) { res.end() } }) }) } else { res.end() } } setTimeout(respond, req.url.includes('slow') ? 200 : 0) }) server.listen(0, '127.0.0.1', () => { server.url = `http://127.0.0.1:${server.address().port}` done() }) }) after(() => { server.close() server = null }) beforeEach(openTheWindow) afterEach(closeTheWindow) describe('BrowserWindow constructor', () => { it('allows passing void 0 as the webContents', () => { openTheWindow({ webContents: void 0 }) }) }) describe('BrowserWindow.close()', () => { let server before((done) => { server = http.createServer((request, response) => { switch (request.url) { case '/404': response.statusCode = '404' response.end() break case '/301': response.statusCode = '301' response.setHeader('Location', '/200') response.end() break case '/200': response.statusCode = '200' response.end('hello') break case '/title': response.statusCode = '200' response.end('Hello') break default: done('unsupported endpoint') } }).listen(0, '127.0.0.1', () => { server.url = 'http://127.0.0.1:' + server.address().port done() }) }) after(() => { server.close() server = null }) it('should emit unload handler', (done) => { w.webContents.on('did-finish-load', () => { w.close() }) w.once('closed', () => { const test = path.join(fixtures, 'api', 'unload') const content = fs.readFileSync(test) fs.unlinkSync(test) assert.equal(String(content), 'unload') done() }) w.loadURL('file://' + path.join(fixtures, 'api', 'unload.html')) }) it('should emit beforeunload handler', (done) => { w.once('onbeforeunload', () => { done() }) w.webContents.on('did-finish-load', () => { w.close() }) w.loadURL(`file://${path.join(fixtures, 'api', 'beforeunload-false.html')}`) }) it('should not crash when invoked synchronously inside navigation observer', (done) => { const events = [ { name: 'did-start-loading', url: `${server.url}/200` }, { name: 'dom-ready', url: `${server.url}/200` }, { name: 'page-title-updated', url: `${server.url}/title` }, { name: 'did-stop-loading', url: `${server.url}/200` }, { name: 'did-finish-load', url: `${server.url}/200` }, { name: 'did-frame-finish-load', url: `${server.url}/200` }, { name: 'did-fail-load', url: `${server.url}/404` } ] const responseEvent = 'window-webContents-destroyed' function * genNavigationEvent () { let eventOptions = null while ((eventOptions = events.shift()) && events.length) { let w = new BrowserWindow({show: false}) eventOptions.id = w.id eventOptions.responseEvent = responseEvent ipcRenderer.send('test-webcontents-navigation-observer', eventOptions) yield 1 } } let gen = genNavigationEvent() ipcRenderer.on(responseEvent, () => { if (!gen.next().value) done() }) gen.next() }) }) describe('window.close()', () => { it('should emit unload handler', (done) => { w.once('closed', () => { const test = path.join(fixtures, 'api', 'close') const content = fs.readFileSync(test) fs.unlinkSync(test) assert.equal(String(content), 'close') done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'close.html')}`) }) it('should emit beforeunload handler', (done) => { w.once('onbeforeunload', () => { done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'close-beforeunload-false.html')}`) }) }) describe('BrowserWindow.destroy()', () => { it('prevents users to access methods of webContents', () => { const contents = w.webContents w.destroy() assert.throws(() => { contents.getProcessId() }, /Object has been destroyed/) }) }) describe('BrowserWindow.loadURL(url)', () => { it('should emit did-start-loading event', (done) => { w.webContents.on('did-start-loading', () => { done() }) w.loadURL('about:blank') }) it('should emit ready-to-show event', (done) => { w.on('ready-to-show', () => { done() }) w.loadURL('about:blank') }) it('should emit did-fail-load event for files that do not exist', (done) => { w.webContents.on('did-fail-load', (event, code, desc, url, isMainFrame) => { assert.equal(code, -6) assert.equal(desc, 'ERR_FILE_NOT_FOUND') assert.equal(isMainFrame, true) done() }) w.loadURL('file://a.txt') }) it('should emit did-fail-load event for invalid URL', (done) => { w.webContents.on('did-fail-load', (event, code, desc, url, isMainFrame) => { assert.equal(desc, 'ERR_INVALID_URL') assert.equal(code, -300) assert.equal(isMainFrame, true) done() }) w.loadURL('http://example:port') }) it('should set `mainFrame = false` on did-fail-load events in iframes', (done) => { w.webContents.on('did-fail-load', (event, code, desc, url, isMainFrame) => { assert.equal(isMainFrame, false) done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'did-fail-load-iframe.html')}`) }) it('does not crash in did-fail-provisional-load handler', (done) => { w.webContents.once('did-fail-provisional-load', () => { w.loadURL('http://127.0.0.1:11111') done() }) w.loadURL('http://127.0.0.1:11111') }) it('should emit did-fail-load event for URL exceeding character limit', (done) => { w.webContents.on('did-fail-load', (event, code, desc, url, isMainFrame) => { assert.equal(desc, 'ERR_INVALID_URL') assert.equal(code, -300) assert.equal(isMainFrame, true) done() }) const data = Buffer.alloc(2 * 1024 * 1024).toString('base64') w.loadURL(`data:image/png;base64,${data}`) }) describe('POST navigations', () => { afterEach(() => { w.webContents.session.webRequest.onBeforeSendHeaders(null) }) it('supports specifying POST data', (done) => { w.webContents.on('did-finish-load', () => done()) w.loadURL(server.url, {postData: postData}) }) it('sets the content type header on URL encoded forms', (done) => { w.webContents.on('did-finish-load', () => { w.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { assert.equal(details.requestHeaders['content-type'], 'application/x-www-form-urlencoded') done() }) w.webContents.executeJavaScript(` form = document.createElement('form') document.body.appendChild(form) form.method = 'POST' form.target = '_blank' form.submit() `) }) w.loadURL(server.url) }) it('sets the content type header on multi part forms', (done) => { w.webContents.on('did-finish-load', () => { w.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { assert(details.requestHeaders['content-type'].startsWith('multipart/form-data; boundary=----WebKitFormBoundary')) done() }) w.webContents.executeJavaScript(` form = document.createElement('form') document.body.appendChild(form) form.method = 'POST' form.target = '_blank' form.enctype = 'multipart/form-data' file = document.createElement('input') file.type = 'file' file.name = 'file' form.appendChild(file) form.submit() `) }) w.loadURL(server.url) }) }) it('should support support base url for data urls', (done) => { ipcMain.once('answer', (event, test) => { assert.equal(test, 'test') done() }) w.loadURL('data:text/html,', {baseURLForDataURL: `file://${path.join(fixtures, 'api')}${path.sep}`}) }) }) describe('will-navigate event', () => { it('allows the window to be closed from the event listener', (done) => { ipcRenderer.send('close-on-will-navigate', w.id) ipcRenderer.once('closed-on-will-navigate', () => { done() }) w.loadURL(`file://${fixtures}/pages/will-navigate.html`) }) }) describe('BrowserWindow.show()', () => { before(function () { if (isCI) { this.skip() } }) it('should focus on window', () => { w.show() assert(w.isFocused()) }) it('should make the window visible', () => { w.show() assert(w.isVisible()) }) it('emits when window is shown', (done) => { w.once('show', () => { assert.equal(w.isVisible(), true) done() }) w.show() }) }) describe('BrowserWindow.hide()', () => { before(function () { if (isCI) { this.skip() } }) it('should defocus on window', () => { w.hide() assert(!w.isFocused()) }) it('should make the window not visible', () => { w.show() w.hide() assert(!w.isVisible()) }) it('emits when window is hidden', (done) => { w.show() w.once('hide', () => { assert.equal(w.isVisible(), false) done() }) w.hide() }) }) describe('BrowserWindow.showInactive()', () => { it('should not focus on window', () => { w.showInactive() assert(!w.isFocused()) }) }) describe('BrowserWindow.focus()', () => { it('does not make the window become visible', () => { assert.equal(w.isVisible(), false) w.focus() assert.equal(w.isVisible(), false) }) }) describe('BrowserWindow.blur()', () => { it('removes focus from window', () => { w.blur() assert(!w.isFocused()) }) }) // TODO(alexeykuzmin): [Ch66] Enable the test. Passes locally. xdescribe('BrowserWindow.getFocusedWindow()', (done) => { it('returns the opener window when dev tools window is focused', (done) => { w.show() w.webContents.once('devtools-focused', () => { assert.deepEqual(BrowserWindow.getFocusedWindow(), w) done() }) w.webContents.openDevTools({mode: 'undocked'}) }) }) describe('BrowserWindow.capturePage(rect, callback)', () => { it('calls the callback with a Buffer', async () => { const image = await new Promise((resolve) => { w.capturePage({ x: 0, y: 0, width: 100, height: 100 }, resolve) }) expect(image.isEmpty()).to.be.true() }) it('preserves transparency', async () => { const w = await openTheWindow({ show: false, width: 400, height: 400, transparent: true }) w.loadURL('data:text/html,') await emittedOnce(w, 'ready-to-show') w.show() const image = await new Promise((resolve) => w.capturePage(resolve)) const imgBuffer = image.toPNG() // Check the 25th byte in the PNG. // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha expect(imgBuffer[25]).to.equal(6) }) }) describe('BrowserWindow.setSize(width, height)', () => { it('sets the window size', async () => { const size = [300, 400] const resized = emittedOnce(w, 'resize') w.setSize(size[0], size[1]) await resized assertBoundsEqual(w.getSize(), size) }) }) describe('BrowserWindow.setMinimum/MaximumSize(width, height)', () => { it('sets the maximum and minimum size of the window', () => { assert.deepEqual(w.getMinimumSize(), [0, 0]) assert.deepEqual(w.getMaximumSize(), [0, 0]) w.setMinimumSize(100, 100) assertBoundsEqual(w.getMinimumSize(), [100, 100]) assertBoundsEqual(w.getMaximumSize(), [0, 0]) w.setMaximumSize(900, 600) assertBoundsEqual(w.getMinimumSize(), [100, 100]) assertBoundsEqual(w.getMaximumSize(), [900, 600]) }) }) describe('BrowserWindow.setAspectRatio(ratio)', () => { it('resets the behaviour when passing in 0', (done) => { const size = [300, 400] w.setAspectRatio(1 / 2) w.setAspectRatio(0) w.once('resize', () => { assertBoundsEqual(w.getSize(), size) done() }) w.setSize(size[0], size[1]) }) }) describe('BrowserWindow.setPosition(x, y)', () => { it('sets the window position', (done) => { const pos = [10, 10] w.once('move', () => { const newPos = w.getPosition() assert.equal(newPos[0], pos[0]) assert.equal(newPos[1], pos[1]) done() }) w.setPosition(pos[0], pos[1]) }) }) describe('BrowserWindow.setContentSize(width, height)', () => { it('sets the content size', () => { const size = [400, 400] w.setContentSize(size[0], size[1]) var after = w.getContentSize() assert.equal(after[0], size[0]) assert.equal(after[1], size[1]) }) it('works for a frameless window', () => { w.destroy() w = new BrowserWindow({ show: false, frame: false, width: 400, height: 400 }) const size = [400, 400] w.setContentSize(size[0], size[1]) const after = w.getContentSize() assert.equal(after[0], size[0]) assert.equal(after[1], size[1]) }) }) describe('BrowserWindow.setContentBounds(bounds)', () => { it('sets the content size and position', (done) => { const bounds = {x: 10, y: 10, width: 250, height: 250} w.once('resize', () => { assertBoundsEqual(w.getContentBounds(), bounds) done() }) w.setContentBounds(bounds) }) it('works for a frameless window', (done) => { w.destroy() w = new BrowserWindow({ show: false, frame: false, width: 300, height: 300 }) const bounds = {x: 10, y: 10, width: 250, height: 250} w.once('resize', () => { assert.deepEqual(w.getContentBounds(), bounds) done() }) w.setContentBounds(bounds) }) }) describe('BrowserWindow.setProgressBar(progress)', () => { it('sets the progress', () => { assert.doesNotThrow(() => { if (process.platform === 'darwin') { app.dock.setIcon(path.join(fixtures, 'assets', 'logo.png')) } w.setProgressBar(0.5) if (process.platform === 'darwin') { app.dock.setIcon(null) } w.setProgressBar(-1) }) }) it('sets the progress using "paused" mode', () => { assert.doesNotThrow(() => { w.setProgressBar(0.5, {mode: 'paused'}) }) }) it('sets the progress using "error" mode', () => { assert.doesNotThrow(() => { w.setProgressBar(0.5, {mode: 'error'}) }) }) it('sets the progress using "normal" mode', () => { assert.doesNotThrow(() => { w.setProgressBar(0.5, {mode: 'normal'}) }) }) }) describe('BrowserWindow.setAlwaysOnTop(flag, level)', () => { it('sets the window as always on top', () => { assert.equal(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true, 'screen-saver') assert.equal(w.isAlwaysOnTop(), true) w.setAlwaysOnTop(false) assert.equal(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true) assert.equal(w.isAlwaysOnTop(), true) }) it('raises an error when relativeLevel is out of bounds', function () { if (process.platform !== 'darwin') { // FIXME(alexeykuzmin): Skip the test instead of marking it as passed. // afterEach hook won't be run if a test is skipped dynamically. // If afterEach isn't run current window won't be destroyed // and the next test will fail on assertion in `closeWindow()`. // this.skip() return } assert.throws(() => { w.setAlwaysOnTop(true, '', -2147483644) }) assert.throws(() => { w.setAlwaysOnTop(true, '', 2147483632) }) }) }) // TODO(alexeykuzmin): [Ch66] Enable the test. Passes locally. xdescribe('BrowserWindow.alwaysOnTop() resets level on minimize', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('resets the windows level on minimize', () => { assert.equal(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true, 'screen-saver') assert.equal(w.isAlwaysOnTop(), true) w.minimize() assert.equal(w.isAlwaysOnTop(), false) w.restore() assert.equal(w.isAlwaysOnTop(), true) }) }) describe('BrowserWindow.setAutoHideCursor(autoHide)', () => { describe('on macOS', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('allows changing cursor auto-hiding', () => { assert.doesNotThrow(() => { w.setAutoHideCursor(false) w.setAutoHideCursor(true) }) }) }) describe('on non-macOS platforms', () => { before(function () { if (process.platform === 'darwin') { this.skip() } }) it('is not available', () => { assert.ok(!w.setAutoHideCursor) }) }) }) describe('BrowserWindow.selectPreviousTab()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.selectPreviousTab() }) }) }) describe('BrowserWindow.selectNextTab()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.selectNextTab() }) }) }) describe('BrowserWindow.mergeAllWindows()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.mergeAllWindows() }) }) }) describe('BrowserWindow.moveTabToNewWindow()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.moveTabToNewWindow() }) }) }) describe('BrowserWindow.toggleTabBar()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.toggleTabBar() }) }) }) describe('BrowserWindow.addTabbedWindow()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', (done) => { const tabbedWindow = new BrowserWindow({}) assert.doesNotThrow(() => { w.addTabbedWindow(tabbedWindow) }) assert.equal(BrowserWindow.getAllWindows().length, 3) // Test window + w + tabbedWindow closeWindow(tabbedWindow, {assertSingleWindow: false}).then(() => { assert.equal(BrowserWindow.getAllWindows().length, 2) // Test window + w done() }) }) it('throws when called on itself', () => { assert.throws(() => { w.addTabbedWindow(w) }, /AddTabbedWindow cannot be called by a window on itself./) }) }) describe('BrowserWindow.setWindowButtonVisibility()', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('does not throw', () => { assert.doesNotThrow(() => { w.setWindowButtonVisibility(true) w.setWindowButtonVisibility(false) }) }) it('throws with custom title bar buttons', () => { assert.throws(() => { w.destroy() w = new BrowserWindow({ show: false, titleBarStyle: 'customButtonsOnHover', frame: false }) w.setWindowButtonVisibility(true) }, /Not supported for this window/) }) }) describe('BrowserWindow.setVibrancy(type)', () => { it('allows setting, changing, and removing the vibrancy', () => { assert.doesNotThrow(() => { w.setVibrancy('light') w.setVibrancy('dark') w.setVibrancy(null) w.setVibrancy('ultra-dark') w.setVibrancy('') }) }) }) describe('BrowserWindow.setAppDetails(options)', () => { before(function () { if (process.platform !== 'win32') { this.skip() } }) it('supports setting the app details', () => { const iconPath = path.join(fixtures, 'assets', 'icon.ico') assert.doesNotThrow(() => { w.setAppDetails({appId: 'my.app.id'}) w.setAppDetails({appIconPath: iconPath, appIconIndex: 0}) w.setAppDetails({appIconPath: iconPath}) w.setAppDetails({relaunchCommand: 'my-app.exe arg1 arg2', relaunchDisplayName: 'My app name'}) w.setAppDetails({relaunchCommand: 'my-app.exe arg1 arg2'}) w.setAppDetails({relaunchDisplayName: 'My app name'}) w.setAppDetails({ appId: 'my.app.id', appIconPath: iconPath, appIconIndex: 0, relaunchCommand: 'my-app.exe arg1 arg2', relaunchDisplayName: 'My app name' }) w.setAppDetails({}) }) assert.throws(() => { w.setAppDetails() }, /Insufficient number of arguments\./) }) }) describe('BrowserWindow.fromId(id)', () => { it('returns the window with id', () => { assert.equal(w.id, BrowserWindow.fromId(w.id).id) }) }) describe('BrowserWindow.fromWebContents(webContents)', () => { let contents = null beforeEach(() => { contents = webContents.create({}) }) afterEach(() => { contents.destroy() }) it('returns the window with the webContents', () => { assert.equal(BrowserWindow.fromWebContents(w.webContents).id, w.id) assert.equal(BrowserWindow.fromWebContents(contents), undefined) }) }) describe('BrowserWindow.fromDevToolsWebContents(webContents)', () => { let contents = null beforeEach(() => { contents = webContents.create({}) }) afterEach(() => { contents.destroy() }) it('returns the window with the webContents', (done) => { w.webContents.once('devtools-opened', () => { assert.equal(BrowserWindow.fromDevToolsWebContents(w.devToolsWebContents).id, w.id) assert.equal(BrowserWindow.fromDevToolsWebContents(w.webContents), undefined) assert.equal(BrowserWindow.fromDevToolsWebContents(contents), undefined) done() }) w.webContents.openDevTools() }) }) describe('BrowserWindow.openDevTools()', () => { it('does not crash for frameless window', () => { w.destroy() w = new BrowserWindow({ show: false }) w.openDevTools() }) }) describe('BrowserWindow.fromBrowserView(browserView)', () => { let bv = null beforeEach(() => { bv = new BrowserView() w.setBrowserView(bv) }) afterEach(() => { w.setBrowserView(null) bv.destroy() }) it('returns the window with the browserView', () => { assert.equal(BrowserWindow.fromBrowserView(bv).id, w.id) }) it('returns undefined if not attached', () => { w.setBrowserView(null) assert.equal(BrowserWindow.fromBrowserView(bv), undefined) }) }) describe('BrowserWindow.setOpacity(opacity)', () => { it('make window with initial opacity', () => { w.destroy() w = new BrowserWindow({ show: false, width: 400, height: 400, opacity: 0.5 }) assert.equal(w.getOpacity(), 0.5) }) it('allows setting the opacity', () => { assert.doesNotThrow(() => { w.setOpacity(0.0) assert.equal(w.getOpacity(), 0.0) w.setOpacity(0.5) assert.equal(w.getOpacity(), 0.5) w.setOpacity(1.0) assert.equal(w.getOpacity(), 1.0) }) }) }) describe('BrowserWindow.setShape(rects)', () => { it('allows setting shape', () => { assert.doesNotThrow(() => { w.setShape([]) w.setShape([{x: 0, y: 0, width: 100, height: 100}]) w.setShape([{x: 0, y: 0, width: 100, height: 100}, {x: 0, y: 200, width: 1000, height: 100}]) w.setShape([]) }) }) }) describe('"useContentSize" option', () => { it('make window created with content size when used', () => { w.destroy() w = new BrowserWindow({ show: false, width: 400, height: 400, useContentSize: true }) const contentSize = w.getContentSize() assert.equal(contentSize[0], 400) assert.equal(contentSize[1], 400) }) it('make window created with window size when not used', () => { const size = w.getSize() assert.equal(size[0], 400) assert.equal(size[1], 400) }) it('works for a frameless window', () => { w.destroy() w = new BrowserWindow({ show: false, frame: false, width: 400, height: 400, useContentSize: true }) const contentSize = w.getContentSize() assert.equal(contentSize[0], 400) assert.equal(contentSize[1], 400) const size = w.getSize() assert.equal(size[0], 400) assert.equal(size[1], 400) }) }) describe('"titleBarStyle" option', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } if (parseInt(os.release().split('.')[0]) < 14) { this.skip() } }) it('creates browser window with hidden title bar', () => { w.destroy() w = new BrowserWindow({ show: false, width: 400, height: 400, titleBarStyle: 'hidden' }) const contentSize = w.getContentSize() assert.equal(contentSize[1], 400) }) it('creates browser window with hidden inset title bar', () => { w.destroy() w = new BrowserWindow({ show: false, width: 400, height: 400, titleBarStyle: 'hiddenInset' }) const contentSize = w.getContentSize() assert.equal(contentSize[1], 400) }) }) describe('enableLargerThanScreen" option', () => { before(function () { if (process.platform === 'linux') { this.skip() } }) beforeEach(() => { w.destroy() w = new BrowserWindow({ show: true, width: 400, height: 400, enableLargerThanScreen: true }) }) it('can move the window out of screen', () => { w.setPosition(-10, -10) const after = w.getPosition() assert.equal(after[0], -10) assert.equal(after[1], -10) }) it('can set the window larger than screen', () => { const size = screen.getPrimaryDisplay().size size.width += 100 size.height += 100 w.setSize(size.width, size.height) assertBoundsEqual(w.getSize(), [size.width, size.height]) }) }) describe('"zoomToPageWidth" option', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('sets the window width to the page width when used', () => { w.destroy() w = new BrowserWindow({ show: false, width: 500, height: 400, zoomToPageWidth: true }) w.maximize() assert.equal(w.getSize()[0], 500) }) }) describe('"tabbingIdentifier" option', () => { it('can be set on a window', () => { w.destroy() w = new BrowserWindow({ tabbingIdentifier: 'group1' }) w.destroy() w = new BrowserWindow({ tabbingIdentifier: 'group2', frame: false }) }) }) describe('"webPreferences" option', () => { afterEach(() => { ipcMain.removeAllListeners('answer') }) describe('"preload" option', () => { it('loads the script before other scripts in window', (done) => { const preload = path.join(fixtures, 'module', 'set-global.js') ipcMain.once('answer', (event, test) => { assert.equal(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload } }) w.loadURL(`file://${path.join(fixtures, 'api', 'preload.html')}`) }) it('can successfully delete the Buffer global', (done) => { const preload = path.join(fixtures, 'module', 'delete-buffer.js') ipcMain.once('answer', (event, test) => { assert.equal(test.toString(), 'buffer') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload } }) w.loadURL(`file://${path.join(fixtures, 'api', 'preload.html')}`) }) }) describe('session preload scripts', function () { const preloads = [ path.join(fixtures, 'module', 'set-global-preload-1.js'), path.join(fixtures, 'module', 'set-global-preload-2.js') ] const defaultSession = session.defaultSession beforeEach(() => { assert.deepEqual(defaultSession.getPreloads(), []) defaultSession.setPreloads(preloads) }) afterEach(() => { defaultSession.setPreloads([]) }) it('can set multiple session preload script', function () { assert.deepEqual(defaultSession.getPreloads(), preloads) }) it('loads the script before other scripts in window including normal preloads', function (done) { ipcMain.once('vars', function (event, preload1, preload2, preload3) { assert.equal(preload1, 'preload-1') assert.equal(preload2, 'preload-1-2') assert.equal(preload3, 'preload-1-2-3') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: path.join(fixtures, 'module', 'set-global-preload-3.js') } }) w.loadURL('file://' + path.join(fixtures, 'api', 'preloads.html')) }) }) describe('"additionalArguments" option', () => { it('adds extra args to process.argv in the renderer process', (done) => { const preload = path.join(fixtures, 'module', 'check-arguments.js') ipcMain.once('answer', (event, argv) => { assert.ok(argv.includes('--my-magic-arg')) done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, additionalArguments: ['--my-magic-arg'] } }) w.loadURL(`file://${path.join(fixtures, 'api', 'blank.html')}`) }) it('adds extra value args to process.argv in the renderer process', (done) => { const preload = path.join(fixtures, 'module', 'check-arguments.js') ipcMain.once('answer', (event, argv) => { assert.ok(argv.includes('--my-magic-arg=foo')) done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, additionalArguments: ['--my-magic-arg=foo'] } }) w.loadURL(`file://${path.join(fixtures, 'api', 'blank.html')}`) }) }) describe('"node-integration" option', () => { it('disables node integration when specified to false', (done) => { const preload = path.join(fixtures, 'module', 'send-later.js') ipcMain.once('answer', (event, typeofProcess, typeofBuffer) => { assert.equal(typeofProcess, 'undefined') assert.equal(typeofBuffer, 'undefined') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, nodeIntegration: false } }) w.loadURL(`file://${path.join(fixtures, 'api', 'blank.html')}`) }) }) describe('"sandbox" option', () => { function waitForEvents (emitter, events, callback) { let count = events.length for (let event of events) { emitter.once(event, () => { if (!--count) callback() }) } } const preload = path.join(fixtures, 'module', 'preload-sandbox.js') // http protocol to simulate accessing another domain. This is required // because the code paths for cross domain popups is different. function crossDomainHandler (request, callback) { // Disabled due to false positive in StandardJS // eslint-disable-next-line standard/no-callback-literal callback({ mimeType: 'text/html', data: `

${request.url}

` }) } before((done) => { protocol.interceptStringProtocol('http', crossDomainHandler, () => { done() }) }) after((done) => { protocol.uninterceptProtocol('http', () => { done() }) }) it('exposes ipcRenderer to preload script', (done) => { ipcMain.once('answer', function (event, test) { assert.equal(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadURL('file://' + path.join(fixtures, 'api', 'preload.html')) }) it('exposes ipcRenderer to preload script (path has special chars)', function (done) { const preloadSpecialChars = path.join(fixtures, 'module', 'preload-sandboxæø åü.js') ipcMain.once('answer', function (event, test) { assert.equal(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preloadSpecialChars } }) w.loadURL('file://' + path.join(fixtures, 'api', 'preload.html')) }) it('exposes "exit" event to preload script', function (done) { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) let htmlPath = path.join(fixtures, 'api', 'sandbox.html?exit-event') const pageUrl = 'file://' + htmlPath w.loadURL(pageUrl) ipcMain.once('answer', function (event, url) { let expectedUrl = pageUrl if (process.platform === 'win32') { expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') } assert.equal(url, expectedUrl) done() }) }) it('should open windows in same domain with cross-scripting enabled', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) let htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open') const pageUrl = 'file://' + htmlPath w.loadURL(pageUrl) w.webContents.once('new-window', (e, url, frameName, disposition, options) => { let expectedUrl = pageUrl if (process.platform === 'win32') { expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') } assert.equal(url, expectedUrl) assert.equal(frameName, 'popup!') assert.equal(options.width, 500) assert.equal(options.height, 600) ipcMain.once('answer', function (event, html) { assert.equal(html, '

scripting from opener

') done() }) }) }) it('should open windows in another domain with cross-scripting disabled', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) let htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open-external') const pageUrl = 'file://' + htmlPath let popupWindow w.loadURL(pageUrl) w.webContents.once('new-window', (e, url, frameName, disposition, options) => { assert.equal(url, 'http://www.google.com/#q=electron') assert.equal(options.width, 505) assert.equal(options.height, 605) ipcMain.once('child-loaded', function (event, openerIsNull, html) { assert(openerIsNull) assert.equal(html, '

http://www.google.com/#q=electron

') ipcMain.once('answer', function (event, exceptionMessage) { assert(/Blocked a frame with origin/.test(exceptionMessage)) // FIXME this popup window should be closed in sandbox.html closeWindow(popupWindow, {assertSingleWindow: false}).then(() => { popupWindow = null done() }) }) w.webContents.send('child-loaded') }) }) app.once('browser-window-created', function (event, window) { popupWindow = window }) }) it('should inherit the sandbox setting in opened windows', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preloadPath) ipcMain.once('answer', (event, args) => { assert.equal(args.includes('--enable-sandbox'), true) done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'new-window.html')}`) }) it('should open windows with the options configured via new-window event listeners', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preloadPath) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'foo', 'bar') ipcMain.once('answer', (event, args, webPreferences) => { assert.equal(webPreferences.foo, 'bar') done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'new-window.html')}`) }) it('should set ipc event sender correctly', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) let htmlPath = path.join(fixtures, 'api', 'sandbox.html?verify-ipc-sender') const pageUrl = 'file://' + htmlPath let childWc w.webContents.once('new-window', (e, url, frameName, disposition, options) => { childWc = options.webContents assert.notEqual(w.webContents, childWc) }) ipcMain.once('parent-ready', function (event) { assert.equal(w.webContents, event.sender) event.sender.send('verified') }) ipcMain.once('child-ready', function (event) { assert(childWc) assert.equal(childWc, event.sender) event.sender.send('verified') }) waitForEvents(ipcMain, [ 'parent-answer', 'child-answer' ], done) w.loadURL(pageUrl) }) describe('event handling', () => { it('works for window events', (done) => { waitForEvents(w, [ 'page-title-updated' ], done) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?window-events')) }) it('works for stop events', (done) => { waitForEvents(w.webContents, [ 'did-navigate', 'did-fail-load', 'did-stop-loading' ], done) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?webcontents-stop')) }) it('works for web contents events', (done) => { waitForEvents(w.webContents, [ 'did-finish-load', 'did-frame-finish-load', 'did-navigate-in-page', 'will-navigate', 'did-start-loading', 'did-stop-loading', 'did-frame-finish-load', 'dom-ready' ], done) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?webcontents-events')) }) }) it('can get printer list', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E') w.webContents.once('did-finish-load', () => { const printers = w.webContents.getPrinters() assert.equal(Array.isArray(printers), true) done() }) }) it('can print to PDF', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E') w.webContents.once('did-finish-load', () => { w.webContents.printToPDF({}, function (error, data) { assert.equal(error, null) assert.equal(data instanceof Buffer, true) assert.notEqual(data.length, 0) done() }) }) }) it('supports calling preventDefault on new-window events', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const initialWebContents = webContents.getAllWebContents().map((i) => i.id) ipcRenderer.send('prevent-next-new-window', w.webContents.id) w.webContents.once('new-window', () => { // We need to give it some time so the windows get properly disposed (at least on OSX). setTimeout(() => { const currentWebContents = webContents.getAllWebContents().map((i) => i.id) assert.deepEqual(currentWebContents, initialWebContents) done() }, 100) }) w.loadURL('file://' + path.join(fixtures, 'pages', 'window-open.html')) }) it('releases memory after popup is closed', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, sandbox: true } }) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?allocate-memory')) ipcMain.once('answer', function (event, {bytesBeforeOpen, bytesAfterOpen, bytesAfterClose}) { const memoryIncreaseByOpen = bytesAfterOpen - bytesBeforeOpen const memoryDecreaseByClose = bytesAfterOpen - bytesAfterClose // decreased memory should be less than increased due to factors we // can't control, but given the amount of memory allocated in the // fixture, we can reasonably expect decrease to be at least 70% of // increase assert(memoryDecreaseByClose > memoryIncreaseByOpen * 0.7) done() }) }) // see #9387 it('properly manages remote object references after page reload', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, sandbox: true } }) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?reload-remote')) ipcMain.on('get-remote-module-path', (event) => { event.returnValue = path.join(fixtures, 'module', 'hello.js') }) let reload = false ipcMain.on('reloaded', (event) => { event.returnValue = reload reload = !reload }) ipcMain.once('reload', (event) => { event.sender.reload() }) ipcMain.once('answer', (event, arg) => { ipcMain.removeAllListeners('reloaded') ipcMain.removeAllListeners('get-remote-module-path') assert.equal(arg, 'hi') done() }) }) it('properly manages remote object references after page reload in child window', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, sandbox: true } }) w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?reload-remote-child')) ipcMain.on('get-remote-module-path', (event) => { event.returnValue = path.join(fixtures, 'module', 'hello-child.js') }) let reload = false ipcMain.on('reloaded', (event) => { event.returnValue = reload reload = !reload }) ipcMain.once('reload', (event) => { event.sender.reload() }) ipcMain.once('answer', (event, arg) => { ipcMain.removeAllListeners('reloaded') ipcMain.removeAllListeners('get-remote-module-path') assert.equal(arg, 'hi child window') done() }) }) it('validates process APIs access in sandboxed renderer', (done) => { ipcMain.once('answer', function (event, test) { assert.equal(test.platform, remote.process.platform) assert.deepEqual(test.env, remote.process.env) assert.equal(test.execPath, remote.process.helperExecPath) done() }) remote.process.env.sandboxmain = 'foo' w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadURL('file://' + path.join(fixtures, 'api', 'preload.html')) }) it('webview in sandbox renderer', async () => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload, webviewTag: true } }) w.loadURL(`file://${fixtures}/pages/webview-did-attach-event.html`) const [, webContents] = await emittedOnce(w.webContents, 'did-attach-webview') const [, id] = await emittedOnce(ipcMain, 'webview-dom-ready') expect(webContents.id).to.equal(id) }) }) describe('nativeWindowOpen option', () => { beforeEach(() => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nativeWindowOpen: true } }) }) it('opens window of about:blank with cross-scripting enabled', (done) => { ipcMain.once('answer', (event, content) => { assert.equal(content, 'Hello') done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}`) }) it('opens window of same domain with cross-scripting enabled', (done) => { ipcMain.once('answer', (event, content) => { assert.equal(content, 'Hello') done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'native-window-open-file.html')}`) }) it('blocks accessing cross-origin frames', (done) => { ipcMain.once('answer', (event, content) => { assert.equal(content, 'Blocked a frame with origin "file://" from accessing a cross-origin frame.') done() }) w.loadURL(`file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}`) }) it('opens window from