'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 { resolveGetters } = require('./assert-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 iw = 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', () => { const parsedData = qs.parse(body) fs.readFile(filePath, (err, data) => { if (err) return if (parsedData.username === 'test' && parsedData.file === data.toString()) { res.end() } }) }) } else if (req.url === '/302') { res.setHeader('Location', '/200') res.statusCode = 302 res.end() } else if (req.url === '/navigate-302') { 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.strictEqual(String(content), 'unload') done() }) w.loadFile(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.loadFile(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) { const w = new BrowserWindow({ show: false }) eventOptions.id = w.id eventOptions.responseEvent = responseEvent ipcRenderer.send('test-webcontents-navigation-observer', eventOptions) yield 1 } } const 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.strictEqual(String(content), 'close') done() }) w.loadFile(path.join(fixtures, 'api', 'close.html')) }) it('should emit beforeunload handler', (done) => { w.once('onbeforeunload', () => { done() }) w.loadFile(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/) }) it('should not crash when destroying windows with pending events', (done) => { const responseEvent = 'destroy-test-completed' ipcRenderer.on(responseEvent, () => done()) ipcRenderer.send('test-browserwindow-destroy', { responseEvent }) }) }) 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.strictEqual(code, -6) assert.strictEqual(desc, 'ERR_FILE_NOT_FOUND') assert.strictEqual(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.strictEqual(desc, 'ERR_INVALID_URL') assert.strictEqual(code, -300) assert.strictEqual(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.strictEqual(isMainFrame, false) done() }) w.loadFile(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.strictEqual(desc, 'ERR_INVALID_URL') assert.strictEqual(code, -300) assert.strictEqual(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.strictEqual(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.strictEqual(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.loadFile(path.join(fixtures, 'pages', 'will-navigate.html')) }) }) describe('will-redirect event', () => { it('is emitted on redirects', (done) => { w.webContents.on('will-redirect', (event, url) => { done() }) w.loadURL(`${server.url}/302`) }) it('is emitted after will-navigate on redirects', (done) => { let navigateCalled = false w.loadURL(`${server.url}/navigate-302`) w.webContents.on('will-navigate', () => { navigateCalled = true }) w.webContents.on('will-redirect', (event, url) => { expect(navigateCalled).to.equal(true, 'should have called will-navigate first') done() }) }) it('is emitted before did-stop-loading on redirects', (done) => { let stopCalled = false w.webContents.on('did-stop-loading', () => { stopCalled = true }) w.webContents.on('will-redirect', (event, url) => { expect(stopCalled).to.equal(false, 'should not have called did-stop-loading first') done() }) w.loadURL(`${server.url}/302`) }) it('allows the window to be closed from the event listener', (done) => { ipcRenderer.send('close-on-will-redirect', w.id) ipcRenderer.once('closed-on-will-redirect', () => { done() }) w.loadURL(`${server.url}/302`) }) it('can be prevented', (done) => { ipcRenderer.send('prevent-will-redirect', w.id) w.webContents.on('will-navigate', (e, url) => { expect(url).to.equal(`${server.url}/302`) }) w.webContents.on('did-stop-loading', () => { expect(w.webContents.getURL()).to.equal( `${server.url}/navigate-302`, 'url should not have changed after navigation event' ) done() }) w.webContents.on('will-redirect', (e, url) => { expect(url).to.equal(`${server.url}/200`) }) w.loadURL(`${server.url}/navigate-302`) }) }) 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.strictEqual(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.strictEqual(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.strictEqual(w.isVisible(), false) w.focus() assert.strictEqual(w.isVisible(), false) }) }) describe('BrowserWindow.blur()', () => { it('removes focus from window', () => { w.blur() assert(!w.isFocused()) }) }) describe('BrowserWindow.getFocusedWindow()', (done) => { it('returns the opener window when dev tools window is focused', (done) => { w.show() w.webContents.once('devtools-focused', () => { assert.deepStrictEqual(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.setBounds(bounds[, animate])', () => { it('sets the window bounds with full bounds', () => { const fullBounds = { x: 440, y: 225, width: 500, height: 400 } w.setBounds(fullBounds) assertBoundsEqual(w.getBounds(), fullBounds) }) it('sets the window bounds with partial bounds', () => { const fullBounds = { x: 440, y: 225, width: 500, height: 400 } w.setBounds(fullBounds) const boundsUpdate = { width: 200 } w.setBounds(boundsUpdate) const expectedBounds = Object.assign(fullBounds, boundsUpdate) assertBoundsEqual(w.getBounds(), expectedBounds) }) }) 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.deepStrictEqual(w.getMinimumSize(), [0, 0]) assert.deepStrictEqual(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.strictEqual(newPos[0], pos[0]) assert.strictEqual(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]) const after = w.getContentSize() assert.strictEqual(after[0], size[0]) assert.strictEqual(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.strictEqual(after[0], size[0]) assert.strictEqual(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.deepStrictEqual(w.getContentBounds(), bounds) done() }) w.setContentBounds(bounds) }) }) describe(`BrowserWindow.getNormalBounds()`, () => { describe(`Normal state`, () => { it(`checks normal bounds after resize`, (done) => { const size = [300, 400] w.once('resize', () => { assertBoundsEqual(w.getNormalBounds(), w.getBounds()) done() }) w.setSize(size[0], size[1]) }) it(`checks normal bounds after move`, (done) => { const pos = [10, 10] w.once('move', () => { assertBoundsEqual(w.getNormalBounds(), w.getBounds()) done() }) w.setPosition(pos[0], pos[1]) }) }) describe(`Maximized state`, () => { before(function () { if (isCI) { this.skip() } }) it(`checks normal bounds when maximized`, (done) => { const bounds = w.getBounds() w.once('maximize', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.maximize() }) it(`checks normal bounds when unmaximized`, (done) => { const bounds = w.getBounds() w.once('maximize', () => { w.unmaximize() }) w.once('unmaximize', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.maximize() }) }) describe(`Minimized state`, () => { before(function () { if (isCI) { this.skip() } }) it(`checks normal bounds when minimized`, (done) => { const bounds = w.getBounds() w.once('minimize', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.minimize() }) it(`checks normal bounds when restored`, (done) => { const bounds = w.getBounds() w.once('minimize', () => { w.restore() }) w.once('restore', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.minimize() }) }) describe(`Fullscreen state`, () => { before(function () { if (isCI) { this.skip() } if (process.platform === 'darwin') { this.skip() } }) it(`checks normal bounds when fullscreen'ed`, (done) => { const bounds = w.getBounds() w.once('enter-full-screen', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.setFullScreen(true) }) it(`checks normal bounds when unfullscreen'ed`, (done) => { const bounds = w.getBounds() w.once('enter-full-screen', () => { w.setFullScreen(false) }) w.once('leave-full-screen', () => { assertBoundsEqual(w.getNormalBounds(), bounds) done() }) w.show() w.setFullScreen(true) }) }) }) 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.strictEqual(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true, 'screen-saver') assert.strictEqual(w.isAlwaysOnTop(), true) w.setAlwaysOnTop(false) assert.strictEqual(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true) assert.strictEqual(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) }) }) }) describe('BrowserWindow.alwaysOnTop() resets level on minimize', () => { before(function () { if (process.platform !== 'darwin') { this.skip() } }) it('resets the windows level on minimize', () => { assert.strictEqual(w.isAlwaysOnTop(), false) w.setAlwaysOnTop(true, 'screen-saver') assert.strictEqual(w.isAlwaysOnTop(), true) w.minimize() assert.strictEqual(w.isAlwaysOnTop(), false) w.restore() assert.strictEqual(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.strictEqual(BrowserWindow.getAllWindows().length, 3) // Test window + w + tabbedWindow closeWindow(tabbedWindow, { assertSingleWindow: false }).then(() => { assert.strictEqual(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.strictEqual(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.strictEqual(BrowserWindow.fromWebContents(w.webContents).id, w.id) assert.strictEqual(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.strictEqual(BrowserWindow.fromDevToolsWebContents(w.devToolsWebContents).id, w.id) assert.strictEqual(BrowserWindow.fromDevToolsWebContents(w.webContents), undefined) assert.strictEqual(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.strictEqual(BrowserWindow.fromBrowserView(bv).id, w.id) }) it('returns undefined if not attached', () => { w.setBrowserView(null) assert.strictEqual(BrowserWindow.fromBrowserView(bv), null) }) }) 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.strictEqual(w.getOpacity(), 0.5) }) it('allows setting the opacity', () => { assert.doesNotThrow(() => { w.setOpacity(0.0) assert.strictEqual(w.getOpacity(), 0.0) w.setOpacity(0.5) assert.strictEqual(w.getOpacity(), 0.5) w.setOpacity(1.0) assert.strictEqual(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.strictEqual(contentSize[0], 400) assert.strictEqual(contentSize[1], 400) }) it('make window created with window size when not used', () => { const size = w.getSize() assert.strictEqual(size[0], 400) assert.strictEqual(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.strictEqual(contentSize[0], 400) assert.strictEqual(contentSize[1], 400) const size = w.getSize() assert.strictEqual(size[0], 400) assert.strictEqual(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.strictEqual(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.strictEqual(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.strictEqual(after[0], -10) assert.strictEqual(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.strictEqual(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.strictEqual(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload } }) w.loadFile(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.strictEqual(test.toString(), 'buffer') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload } }) w.loadFile(path.join(fixtures, 'api', 'preload.html')) }) it('has synchronous access to all eventual window APIs', async () => { const preload = path.join(fixtures, 'module', 'access-blink-apis.js') const w = await openTheWindow({ show: false, webPreferences: { preload: preload } }) w.loadFile(path.join(fixtures, 'api', 'preload.html')) const [, test] = await emittedOnce(ipcMain, 'answer') expect(test).to.be.an('object') expect(test.atPreload).to.be.an('array') expect(test.atLoad).to.be.an('array') expect(test.atPreload).to.deep.equal(test.atLoad, 'should have access to the same window APIs') }) }) 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.deepStrictEqual(defaultSession.getPreloads(), []) defaultSession.setPreloads(preloads) }) afterEach(() => { defaultSession.setPreloads([]) }) it('can set multiple session preload script', function () { assert.deepStrictEqual(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.strictEqual(preload1, 'preload-1') assert.strictEqual(preload2, 'preload-1-2') assert.strictEqual(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.loadFile(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.loadFile(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.loadFile(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.strictEqual(typeofProcess, 'undefined') assert.strictEqual(typeofBuffer, 'undefined') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, nodeIntegration: false } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) }) }) describe('"enableRemoteModule" option', () => { const generateSpecs = (description, sandbox) => { describe(description, () => { const preload = path.join(fixtures, 'module', 'preload-remote.js') it('enables the remote module by default', async () => { const w = await openTheWindow({ show: false, webPreferences: { nodeIntegration: false, preload, sandbox } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) const [, remote] = await emittedOnce(ipcMain, 'remote') expect(remote).to.equal('object') }) it('disables the remote module when false', async () => { const w = await openTheWindow({ show: false, webPreferences: { nodeIntegration: false, preload, sandbox, enableRemoteModule: false } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) const [, remote] = await emittedOnce(ipcMain, 'remote') expect(remote).to.equal('undefined') }) }) } generateSpecs('without sandbox', false) generateSpecs('with sandbox', true) }) describe('"sandbox" option', () => { function waitForEvents (emitter, events, callback) { let count = events.length for (const 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.strictEqual(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadFile(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.strictEqual(test, 'preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preloadSpecialChars } }) w.loadFile(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 } }) const 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.strictEqual(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) const 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.strictEqual(url, expectedUrl) assert.strictEqual(frameName, 'popup!') assert.strictEqual(options.width, 500) assert.strictEqual(options.height, 600) ipcMain.once('answer', function (event, html) { assert.strictEqual(html, '

scripting from opener

') done() }) }) }) it('should open windows in another domain with cross-scripting disabled', async () => { const w = await openTheWindow({ show: false, webPreferences: { sandbox: true, preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'window-open-external' }) const expectedPopupUrl = 'http://www.google.com/#q=electron' // Set in the "sandbox.html". // The page is going to open a popup that it won't be able to close. // We have to close it from here later. // XXX(alexeykuzmin): It will leak if the test fails too soon. const [, popupWindow] = await emittedOnce(app, 'browser-window-created') // Wait for a message from the popup's preload script. const [, openerIsNull, html, locationHref] = await emittedOnce(ipcMain, 'child-loaded') expect(openerIsNull).to.be.true('window.opener is not null') expect(html).to.equal(`

${expectedPopupUrl}

`, 'looks like a http: request has not been intercepted locally') expect(locationHref).to.equal(expectedPopupUrl) // Ask the page to access the popup. w.webContents.send('touch-the-popup') const [, exceptionMessage] = await emittedOnce(ipcMain, 'answer') // We don't need the popup anymore, and its parent page can't close it, // so let's close it from here before we run any checks. await closeWindow(popupWindow, { assertSingleWindow: false }) expect(exceptionMessage).to.be.a('string', `child's .document is accessible from its parent window`) expect(exceptionMessage).to.match(/^Blocked a frame with origin/) }) 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.strictEqual(args.includes('--enable-sandbox'), true) done() }) w.loadFile(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.strictEqual(webPreferences.foo, 'bar') done() }) w.loadFile(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 } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) let childWc w.webContents.once('new-window', (e, url, frameName, disposition, options) => { childWc = options.webContents assert.notStrictEqual(w.webContents, childWc) }) ipcMain.once('parent-ready', function (event) { assert.strictEqual(w.webContents, event.sender) event.sender.send('verified') }) ipcMain.once('child-ready', function (event) { assert(childWc) assert.strictEqual(childWc, event.sender) event.sender.send('verified') }) waitForEvents(ipcMain, [ 'parent-answer', 'child-answer' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'verify-ipc-sender' }) }) describe('event handling', () => { it('works for window events', (done) => { waitForEvents(w, [ 'page-title-updated' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'window-events' }) }) it('works for stop events', (done) => { waitForEvents(w.webContents, [ 'did-navigate', 'did-fail-load', 'did-stop-loading' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: '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.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'webcontents-events' }) }) }) 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.deepStrictEqual(currentWebContents, initialWebContents) done() }, 100) }) w.loadFile(path.join(fixtures, 'pages', 'window-open.html')) }) // TODO(alexeykuzmin): `GetProcessMemoryInfo()` is not available starting Ch67. xit('releases memory after popup is closed', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload: preload, sandbox: true } }) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: '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.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: '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.strictEqual(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 } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: '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.strictEqual(arg, 'hi child window') done() }) }) it('validates process APIs access in sandboxed renderer', (done) => { ipcMain.once('answer', function (event, test) { assert.strictEqual(test.pid, w.webContents.getOSProcessId()) assert.strictEqual(test.arch, remote.process.arch) assert.strictEqual(test.platform, remote.process.platform) assert.deepStrictEqual(...resolveGetters(test.env, remote.process.env)) assert.strictEqual(test.execPath, remote.process.helperExecPath) assert.strictEqual(test.sandboxed, true) assert.strictEqual(test.type, 'renderer') assert.strictEqual(test.version, remote.process.version) assert.deepStrictEqual(test.versions, remote.process.versions) done() }) remote.process.env.sandboxmain = 'foo' w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preload } }) w.loadFile(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.loadFile(path.join(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.strictEqual(content, 'Hello') done() }) w.loadFile(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.strictEqual(content, 'Hello') done() }) w.loadFile(path.join(fixtures, 'api', 'native-window-open-file.html')) }) it('blocks accessing cross-origin frames', (done) => { ipcMain.once('answer', (event, content) => { assert.strictEqual(content, 'Blocked a frame with origin "file://" from accessing a cross-origin frame.') done() }) w.loadFile(path.join(fixtures, 'api', 'native-window-open-cross-origin.html')) }) it('opens window from