diff --git a/spec-main/api-browser-window-spec.ts b/spec-main/api-browser-window-spec.ts index 691d96769a8f..c2f7d45fab23 100644 --- a/spec-main/api-browser-window-spec.ts +++ b/spec-main/api-browser-window-spec.ts @@ -2448,15 +2448,12 @@ describe('BrowserWindow module', () => { throw new Error(`Unexpected visibility change event. visibilityState: ${visibilityState} hidden: ${hidden}`) }) try { - console.log("c 1") const shown1 = emittedOnce(w, 'show') w.show() await shown1 - console.log("c 2") const hidden = emittedOnce(w, 'hide') w.hide() await hidden - console.log("c 3") const shown2 = emittedOnce(w, 'show') w.show() await shown2 @@ -2682,31 +2679,24 @@ describe('BrowserWindow module', () => { ifdescribe(process.platform === 'win32')('on windows', () => { it('should restore a normal visible window from a fullscreen startup state', async () => { const w = new BrowserWindow({show: false}) - console.log("a 1") await w.loadURL('about:blank') - console.log("a 2") const shown = emittedOnce(w, 'show') // start fullscreen and hidden w.setFullScreen(true) w.show() await shown - console.log("a 3") const leftFullScreen = emittedOnce(w, 'leave-full-screen') w.setFullScreen(false) await leftFullScreen - console.log("a 4") expect(w.isVisible()).to.be.true('visible') expect(w.isFullScreen()).to.be.false('fullscreen') }) it('should keep window hidden if already in hidden state', async () => { const w = new BrowserWindow({show: false}) - console.log("b 1") await w.loadURL('about:blank') - console.log("b 2") const leftFullScreen = emittedOnce(w, 'leave-full-screen') w.setFullScreen(false) await leftFullScreen - console.log("b 3") expect(w.isVisible()).to.be.false('visible') expect(w.isFullScreen()).to.be.false('fullscreen') }) @@ -3328,4 +3318,477 @@ describe('BrowserWindow module', () => { }) }) + ifdescribe(!process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS)('window.getNativeWindowHandle()', () => { + afterEach(closeAllWindows) + it('returns valid handle', () => { + const w = new BrowserWindow({show: false}) + // The module's source code is hosted at + // https://github.com/electron/node-is-valid-window + const isValidWindow = require('is-valid-window') + expect(isValidWindow(w.getNativeWindowHandle())).to.be.true('is valid window') + }) + }) + + describe('extensions and dev tools extensions', () => { + let showPanelTimeoutId: NodeJS.Timeout | null = null + + const showLastDevToolsPanel = (w: BrowserWindow) => { + w.webContents.once('devtools-opened', () => { + const show = () => { + if (w == null || w.isDestroyed()) return + const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined } + if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) { + return + } + + const showLastPanel = () => { + // this is executed in the devtools context, where UI is a global + const {UI} = (window as any) + const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id + UI.inspectorView.showPanel(lastPanelId) + } + devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => { + showPanelTimeoutId = setTimeout(show, 100) + }) + } + showPanelTimeoutId = setTimeout(show, 100) + }) + } + + afterEach(() => { + if (showPanelTimeoutId != null) { + clearTimeout(showPanelTimeoutId) + showPanelTimeoutId = null + } + }) + + describe('BrowserWindow.addDevToolsExtension', () => { + describe('for invalid extensions', () => { + it('throws errors for missing manifest.json files', () => { + const nonexistentExtensionPath = path.join(__dirname, 'does-not-exist') + expect(() => { + BrowserWindow.addDevToolsExtension(nonexistentExtensionPath) + }).to.throw(/ENOENT: no such file or directory/) + }) + + it('throws errors for invalid manifest.json files', () => { + const badManifestExtensionPath = path.join(fixtures, 'devtools-extensions', 'bad-manifest') + expect(() => { + BrowserWindow.addDevToolsExtension(badManifestExtensionPath) + }).to.throw(/Unexpected token }/) + }) + }) + + describe('for a valid extension', () => { + const extensionName = 'foo' + + before(() => { + const extensionPath = path.join(fixtures, 'devtools-extensions', 'foo') + BrowserWindow.addDevToolsExtension(extensionPath) + expect(BrowserWindow.getDevToolsExtensions()).to.have.property(extensionName) + }) + + after(() => { + BrowserWindow.removeDevToolsExtension('foo') + expect(BrowserWindow.getDevToolsExtensions()).to.not.have.property(extensionName) + }) + + describe('when the devtools is docked', () => { + let message: any + let w: BrowserWindow + before(async () => { + w = new BrowserWindow({show: false, webPreferences: {nodeIntegration: true}}) + const p = new Promise(resolve => ipcMain.once('answer', (event, message) => { + resolve(message) + })) + showLastDevToolsPanel(w) + w.loadURL('about:blank') + w.webContents.openDevTools({ mode: 'bottom' }) + message = await p + }) + after(closeAllWindows) + + describe('created extension info', function () { + it('has proper "runtimeId"', async function () { + expect(message).to.have.ownProperty('runtimeId') + expect(message.runtimeId).to.equal(extensionName) + }) + it('has "tabId" matching webContents id', function () { + expect(message).to.have.ownProperty('tabId') + expect(message.tabId).to.equal(w.webContents.id) + }) + it('has "i18nString" with proper contents', function () { + expect(message).to.have.ownProperty('i18nString') + expect(message.i18nString).to.equal('foo - bar (baz)') + }) + it('has "storageItems" with proper contents', function () { + expect(message).to.have.ownProperty('storageItems') + expect(message.storageItems).to.deep.equal({ + local: { + set: { hello: 'world', world: 'hello' }, + remove: { world: 'hello' }, + clear: {} + }, + sync: { + set: { foo: 'bar', bar: 'foo' }, + remove: { foo: 'bar' }, + clear: {} + } + }) + }) + }) + }) + + describe('when the devtools is undocked', () => { + let message: any + let w: BrowserWindow + before(async () => { + w = new BrowserWindow({show: false, webPreferences: {nodeIntegration: true}}) + showLastDevToolsPanel(w) + w.loadURL('about:blank') + w.webContents.openDevTools({ mode: 'undocked' }) + message = await new Promise(resolve => ipcMain.once('answer', (event, message) => { + resolve(message) + })) + }) + after(closeAllWindows) + + describe('created extension info', function () { + it('has proper "runtimeId"', function () { + expect(message).to.have.ownProperty('runtimeId') + expect(message.runtimeId).to.equal(extensionName) + }) + it('has "tabId" matching webContents id', function () { + expect(message).to.have.ownProperty('tabId') + expect(message.tabId).to.equal(w.webContents.id) + }) + }) + }) + }) + }) + + it('works when used with partitions', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + partition: 'temp' + } + }) + + const extensionPath = path.join(fixtures, 'devtools-extensions', 'foo') + BrowserWindow.addDevToolsExtension(extensionPath) + try { + showLastDevToolsPanel(w) + + const p: Promise = new Promise(resolve => ipcMain.once('answer', function (event, message) { + resolve(message) + })) + + w.loadURL('about:blank') + w.webContents.openDevTools({ mode: 'bottom' }) + const message = await p + expect(message.runtimeId).to.equal('foo') + } finally { + BrowserWindow.removeDevToolsExtension('foo') + await closeAllWindows() + } + }) + + it('serializes the registered extensions on quit', () => { + const extensionName = 'foo' + const extensionPath = path.join(fixtures, 'devtools-extensions', extensionName) + const serializedPath = path.join(app.getPath('userData'), 'DevTools Extensions') + + BrowserWindow.addDevToolsExtension(extensionPath) + app.emit('will-quit') + expect(JSON.parse(fs.readFileSync(serializedPath, 'utf8'))).to.deep.equal([extensionPath]) + + BrowserWindow.removeDevToolsExtension(extensionName) + app.emit('will-quit') + expect(fs.existsSync(serializedPath)).to.be.false('file exists') + }) + + describe('BrowserWindow.addExtension', () => { + it('throws errors for missing manifest.json files', () => { + expect(() => { + BrowserWindow.addExtension(path.join(__dirname, 'does-not-exist')) + }).to.throw('ENOENT: no such file or directory') + }) + + it('throws errors for invalid manifest.json files', () => { + expect(() => { + BrowserWindow.addExtension(path.join(fixtures, 'devtools-extensions', 'bad-manifest')) + }).to.throw('Unexpected token }') + }) + }) + }) + + ifdescribe(process.platform === 'darwin')('previewFile', () => { + afterEach(closeAllWindows) + it('opens the path in Quick Look on macOS', () => { + const w = new BrowserWindow({show: false}) + expect(() => { + w.previewFile(__filename) + w.closeFilePreview() + }).to.not.throw() + }) + }) + + describe('contextIsolation option with and without sandbox option', () => { + const expectedContextData = { + preloadContext: { + preloadProperty: 'number', + pageProperty: 'undefined', + typeofRequire: 'function', + typeofProcess: 'object', + typeofArrayPush: 'function', + typeofFunctionApply: 'function', + typeofPreloadExecuteJavaScriptProperty: 'undefined' + }, + pageContext: { + preloadProperty: 'undefined', + pageProperty: 'string', + typeofRequire: 'undefined', + typeofProcess: 'undefined', + typeofArrayPush: 'number', + typeofFunctionApply: 'boolean', + typeofPreloadExecuteJavaScriptProperty: 'number', + typeofOpenedWindow: 'object' + } + } + + afterEach(closeAllWindows) + + it('separates the page context from the Electron/preload context', async () => { + const iw = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + const p = emittedOnce(ipcMain, 'isolated-world') + iw.loadFile(path.join(fixtures, 'api', 'isolated.html')) + const [, data] = await p + expect(data).to.deep.equal(expectedContextData) + }) + it('recreates the contexts on reload', async () => { + const iw = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + await iw.loadFile(path.join(fixtures, 'api', 'isolated.html')) + const isolatedWorld = emittedOnce(ipcMain, 'isolated-world') + iw.webContents.reload() + const [, data] = await isolatedWorld + expect(data).to.deep.equal(expectedContextData) + }) + it('enables context isolation on child windows', async () => { + const iw = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + const browserWindowCreated = emittedOnce(app, 'browser-window-created') + iw.loadFile(path.join(fixtures, 'pages', 'window-open.html')) + const [, window] = await browserWindowCreated + expect(window.webContents.getLastWebPreferences().contextIsolation).to.be.true('contextIsolation') + }) + it('separates the page context from the Electron/preload context with sandbox on', async () => { + const ws = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + const p = emittedOnce(ipcMain, 'isolated-world') + ws.loadFile(path.join(fixtures, 'api', 'isolated.html')) + const [, data] = await p + expect(data).to.deep.equal(expectedContextData) + }) + it('recreates the contexts on reload with sandbox on', async () => { + const ws = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + await ws.loadFile(path.join(fixtures, 'api', 'isolated.html')) + const isolatedWorld = emittedOnce(ipcMain, 'isolated-world') + ws.webContents.reload() + const [, data] = await isolatedWorld + expect(data).to.deep.equal(expectedContextData) + }) + it('supports fetch api', async () => { + const fetchWindow = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-fetch-preload.js') + } + }) + const p = emittedOnce(ipcMain, 'isolated-fetch-error') + fetchWindow.loadURL('about:blank') + const [, error] = await p + expect(error).to.equal('Failed to fetch') + }) + it('doesn\'t break ipc serialization', async () => { + const iw = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.join(fixtures, 'api', 'isolated-preload.js') + } + }) + const p = emittedOnce(ipcMain, 'isolated-world') + iw.loadURL('about:blank') + iw.webContents.executeJavaScript(` + const opened = window.open() + openedLocation = opened.location.href + opened.close() + window.postMessage({openedLocation}, '*') + `) + const [, data] = await p + expect(data.pageContext.openedLocation).to.equal('') + }) + }) + + const features = process.electronBinding('features') + ifdescribe(features.isOffscreenRenderingEnabled())('offscreen rendering', () => { + let w: BrowserWindow + beforeEach(function () { + w = new BrowserWindow({ + width: 100, + height: 100, + show: false, + webPreferences: { + backgroundThrottling: false, + offscreen: true + } + }) + }) + afterEach(closeAllWindows) + + it('creates offscreen window with correct size', (done) => { + w.webContents.once('paint', function (event, rect, data) { + expect(data.constructor.name).to.equal('NativeImage') + expect(data.isEmpty()).to.be.false('data is empty') + const size = data.getSize() + const { scaleFactor } = screen.getPrimaryDisplay() + expect(size.width).to.be.closeTo(100 * scaleFactor, 2) + expect(size.height).to.be.closeTo(100 * scaleFactor, 2) + done() + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + + it('does not crash after navigation', () => { + w.webContents.loadURL('about:blank') + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + + describe('window.webContents.isOffscreen()', () => { + it('is true for offscreen type', () => { + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + expect(w.webContents.isOffscreen()).to.be.true('isOffscreen') + }) + + it('is false for regular window', () => { + const c = new BrowserWindow({ show: false }) + expect(c.webContents.isOffscreen()).to.be.false('isOffscreen') + c.destroy() + }) + }) + + describe('window.webContents.isPainting()', () => { + it('returns whether is currently painting', (done) => { + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.isPainting()).to.be.true('isPainting') + done() + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + + describe('window.webContents.stopPainting()', () => { + it('stops painting', (done) => { + w.webContents.on('dom-ready', () => { + w.webContents.stopPainting() + expect(w.webContents.isPainting()).to.be.false('isPainting') + done() + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + + describe('window.webContents.startPainting()', () => { + it('starts painting', (done) => { + w.webContents.on('dom-ready', () => { + w.webContents.stopPainting() + w.webContents.startPainting() + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.isPainting()).to.be.true('isPainting') + done() + }) + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + + // TODO(codebytere): remove in Electron v8.0.0 + describe('window.webContents.getFrameRate()', () => { + it('has default frame rate', (done) => { + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.getFrameRate()).to.equal(60) + done() + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + + // TODO(codebytere): remove in Electron v8.0.0 + describe('window.webContents.setFrameRate(frameRate)', () => { + it('sets custom frame rate', (done) => { + w.webContents.on('dom-ready', () => { + w.webContents.setFrameRate(30) + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.getFrameRate()).to.equal(30) + done() + }) + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + + describe('window.webContents.FrameRate', () => { + it('has default frame rate', (done) => { + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.frameRate).to.equal(60) + done() + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + + it('sets custom frame rate', (done) => { + w.webContents.on('dom-ready', () => { + w.webContents.frameRate = 30 + w.webContents.once('paint', function (event, rect, data) { + expect(w.webContents.frameRate).to.equal(30) + done() + }) + }) + w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) + }) + }) + }) }) diff --git a/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index bb84ad59880a..6db667cf493c 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -1,9 +1,11 @@ import * as chai from 'chai' +import { AddressInfo } from 'net' import * as chaiAsPromised from 'chai-as-promised' import * as path from 'path' +import * as http from 'http' import { BrowserWindow, webContents } from 'electron' import { emittedOnce } from './events-helpers'; -import { closeWindow } from './window-helpers'; +import { closeAllWindows } from './window-helpers'; const { expect } = chai @@ -12,28 +14,13 @@ chai.use(chaiAsPromised) const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures') describe('webContents module', () => { - let w: BrowserWindow = (null as unknown as BrowserWindow) - - beforeEach(() => { - w = new BrowserWindow({ - show: false, - width: 400, - height: 400, - webPreferences: { - backgroundThrottling: false, - nodeIntegration: true, - webviewTag: true - } - }) - }) - - afterEach(async () => { - await closeWindow(w) - w = (null as unknown as BrowserWindow) - }) - describe('getAllWebContents() API', () => { + afterEach(closeAllWindows) it('returns an array of web contents', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { webviewTag: true } + }) w.loadFile(path.join(fixturesPath, 'pages', 'webview-zoom-factor.html')) await emittedOnce(w.webContents, 'did-attach-webview') @@ -54,7 +41,9 @@ describe('webContents module', () => { }) describe('will-prevent-unload event', () => { + afterEach(closeAllWindows) it('does not emit if beforeunload returns undefined', (done) => { + const w = new BrowserWindow({show: false}) w.once('closed', () => done()) w.webContents.once('will-prevent-unload', (e) => { expect.fail('should not have fired') @@ -63,14 +52,131 @@ describe('webContents module', () => { }) it('emits if beforeunload returns false', (done) => { + const w = new BrowserWindow({show: false}) w.webContents.once('will-prevent-unload', () => done()) w.loadFile(path.join(fixturesPath, 'api', 'close-beforeunload-false.html')) }) it('supports calling preventDefault on will-prevent-unload events', (done) => { + const w = new BrowserWindow({show: false}) w.webContents.once('will-prevent-unload', event => event.preventDefault()) w.once('closed', () => done()) w.loadFile(path.join(fixturesPath, 'api', 'close-beforeunload-false.html')) }) }) + + describe('webContents.send(channel, args...)', () => { + afterEach(closeAllWindows) + it('throws an error when the channel is missing', () => { + const w = new BrowserWindow({show: false}) + expect(() => { + (w.webContents.send as any)() + }).to.throw('Missing required channel argument') + + expect(() => { + w.webContents.send(null as any) + }).to.throw('Missing required channel argument') + }) + }) + + describe('webContents.executeJavaScript', () => { + describe('in about:blank', () => { + const expected = 'hello, world!' + const expectedErrorMsg = 'woops!' + const code = `(() => "${expected}")()` + const asyncCode = `(() => new Promise(r => setTimeout(() => r("${expected}"), 500)))()` + const badAsyncCode = `(() => new Promise((r, e) => setTimeout(() => e("${expectedErrorMsg}"), 500)))()` + const errorTypes = new Set([ + Error, + ReferenceError, + EvalError, + RangeError, + SyntaxError, + TypeError, + URIError + ]) + let w: BrowserWindow + + before(async () => { + w = new BrowserWindow({show: false}) + await w.loadURL('about:blank') + }) + after(closeAllWindows) + + it('resolves the returned promise with the result', async () => { + const result = await w.webContents.executeJavaScript(code) + expect(result).to.equal(expected) + }) + it('resolves the returned promise with the result if the code returns an asyncronous promise', async () => { + const result = await w.webContents.executeJavaScript(asyncCode) + expect(result).to.equal(expected) + }) + it('rejects the returned promise if an async error is thrown', async () => { + await expect(w.webContents.executeJavaScript(badAsyncCode)).to.eventually.be.rejectedWith(expectedErrorMsg) + }) + it('rejects the returned promise with an error if an Error.prototype is thrown', async () => { + for (const error of errorTypes) { + await expect(w.webContents.executeJavaScript(`Promise.reject(new ${error.name}("Wamp-wamp"))`)) + .to.eventually.be.rejectedWith(error) + } + }) + }) + + describe("on a real page", () => { + let w: BrowserWindow + beforeEach(() => { + w = new BrowserWindow({show: false}) + }) + afterEach(closeAllWindows) + + let server: http.Server = null as unknown as http.Server + let serverUrl: string = null as unknown as string + + before((done) => { + server = http.createServer((request, response) => { + response.end() + }).listen(0, '127.0.0.1', () => { + serverUrl = 'http://127.0.0.1:' + (server.address() as AddressInfo).port + done() + }) + }) + + after(() => { + server.close() + }) + + it('works after page load and during subframe load', (done) => { + w.webContents.once('did-finish-load', () => { + // initiate a sub-frame load, then try and execute script during it + w.webContents.executeJavaScript(` + var iframe = document.createElement('iframe') + iframe.src = '${serverUrl}/slow' + document.body.appendChild(iframe) + `).then(() => { + w.webContents.executeJavaScript('console.log(\'hello\')').then(() => { + done() + }) + }) + }) + w.loadURL(serverUrl) + }) + + it('executes after page load', (done) => { + w.webContents.executeJavaScript(`(() => "test")()`).then(result => { + expect(result).to.equal("test") + done() + }) + w.loadURL(serverUrl) + }) + + it('works with result objects that have DOM class prototypes', (done) => { + w.webContents.executeJavaScript('document.location').then(result => { + expect(result.origin).to.equal(serverUrl) + expect(result.protocol).to.equal('http:') + done() + }) + w.loadURL(serverUrl) + }) + }) + }) }) diff --git a/spec/api-browser-window-spec.js b/spec/api-browser-window-spec.js deleted file mode 100644 index 3b7e14c59f08..000000000000 --- a/spec/api-browser-window-spec.js +++ /dev/null @@ -1,704 +0,0 @@ -'use strict' - -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 } = require('electron') -const { app, ipcMain, BrowserWindow, BrowserView, protocol, session, screen, webContents } = remote - -const features = process.electronBinding('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, - nodeIntegration: true - } - } - - 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 if (req.url === '/cross-site') { - res.end(`

${req.url}

`) - } 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('window.webContents.send(channel, args...)', () => { - it('throws an error when the channel is missing', () => { - expect(() => { - w.webContents.send() - }).to.throw('Missing required channel argument') - - expect(() => { - w.webContents.send(null) - }).to.throw('Missing required channel argument') - }) - }) - - describe('window.getNativeWindowHandle()', () => { - before(function () { - if (!nativeModulesEnabled) { - this.skip() - } - }) - - it('returns valid handle', () => { - // The module's source code is hosted at - // https://github.com/electron/node-is-valid-window - const isValidWindow = remote.require('is-valid-window') - expect(isValidWindow(w.getNativeWindowHandle())).to.be.true() - }) - }) - - describe('extensions and dev tools extensions', () => { - let showPanelTimeoutId - - const showLastDevToolsPanel = () => { - w.webContents.once('devtools-opened', () => { - const show = () => { - if (w == null || w.isDestroyed()) return - const { devToolsWebContents } = w - if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) { - return - } - - const showLastPanel = () => { - const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id - UI.inspectorView.showPanel(lastPanelId) - } - devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => { - showPanelTimeoutId = setTimeout(show, 100) - }) - } - showPanelTimeoutId = setTimeout(show, 100) - }) - } - - afterEach(() => { - clearTimeout(showPanelTimeoutId) - }) - - describe('BrowserWindow.addDevToolsExtension', () => { - describe('for invalid extensions', () => { - it('throws errors for missing manifest.json files', () => { - const nonexistentExtensionPath = path.join(__dirname, 'does-not-exist') - expect(() => { - BrowserWindow.addDevToolsExtension(nonexistentExtensionPath) - }).to.throw(/ENOENT: no such file or directory/) - }) - - it('throws errors for invalid manifest.json files', () => { - const badManifestExtensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest') - expect(() => { - BrowserWindow.addDevToolsExtension(badManifestExtensionPath) - }).to.throw(/Unexpected token }/) - }) - }) - - describe('for a valid extension', () => { - const extensionName = 'foo' - - const removeExtension = () => { - BrowserWindow.removeDevToolsExtension('foo') - expect(BrowserWindow.getDevToolsExtensions()).to.not.have.a.property(extensionName) - } - - const addExtension = () => { - const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo') - BrowserWindow.addDevToolsExtension(extensionPath) - expect(BrowserWindow.getDevToolsExtensions()).to.have.a.property(extensionName) - - showLastDevToolsPanel() - - w.loadURL('about:blank') - } - - // After* hooks won't be called if a test fail. - // So let's make a clean-up in the before hook. - beforeEach(removeExtension) - - describe('when the devtools is docked', () => { - beforeEach(function (done) { - addExtension() - w.webContents.openDevTools({ mode: 'bottom' }) - ipcMain.once('answer', (event, message) => { - this.message = message - done() - }) - }) - - describe('created extension info', function () { - it('has proper "runtimeId"', function () { - expect(this.message).to.have.own.property('runtimeId') - expect(this.message.runtimeId).to.equal(extensionName) - }) - it('has "tabId" matching webContents id', function () { - expect(this.message).to.have.own.property('tabId') - expect(this.message.tabId).to.equal(w.webContents.id) - }) - it('has "i18nString" with proper contents', function () { - expect(this.message).to.have.own.property('i18nString') - expect(this.message.i18nString).to.equal('foo - bar (baz)') - }) - it('has "storageItems" with proper contents', function () { - expect(this.message).to.have.own.property('storageItems') - expect(this.message.storageItems).to.deep.equal({ - local: { - set: { hello: 'world', world: 'hello' }, - remove: { world: 'hello' }, - clear: {} - }, - sync: { - set: { foo: 'bar', bar: 'foo' }, - remove: { foo: 'bar' }, - clear: {} - } - }) - }) - }) - }) - - describe('when the devtools is undocked', () => { - beforeEach(function (done) { - addExtension() - w.webContents.openDevTools({ mode: 'undocked' }) - ipcMain.once('answer', (event, message, extensionId) => { - this.message = message - done() - }) - }) - - describe('created extension info', function () { - it('has proper "runtimeId"', function () { - expect(this.message).to.have.own.property('runtimeId') - expect(this.message.runtimeId).to.equal(extensionName) - }) - it('has "tabId" matching webContents id', function () { - expect(this.message).to.have.own.property('tabId') - expect(this.message.tabId).to.equal(w.webContents.id) - }) - }) - }) - }) - }) - - it('works when used with partitions', (done) => { - if (w != null) { - w.destroy() - } - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - partition: 'temp' - } - }) - - const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo') - BrowserWindow.removeDevToolsExtension('foo') - BrowserWindow.addDevToolsExtension(extensionPath) - - showLastDevToolsPanel() - - ipcMain.once('answer', function (event, message) { - expect(message.runtimeId).to.equal('foo') - done() - }) - - w.loadURL('about:blank') - w.webContents.openDevTools({ mode: 'bottom' }) - }) - - it('serializes the registered extensions on quit', () => { - const extensionName = 'foo' - const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', extensionName) - const serializedPath = path.join(app.getPath('userData'), 'DevTools Extensions') - - BrowserWindow.addDevToolsExtension(extensionPath) - app.emit('will-quit') - expect(JSON.parse(fs.readFileSync(serializedPath))).to.deep.equal([extensionPath]) - - BrowserWindow.removeDevToolsExtension(extensionName) - app.emit('will-quit') - expect(fs.existsSync(serializedPath)).to.be.false() - }) - - describe('BrowserWindow.addExtension', () => { - beforeEach(() => { - BrowserWindow.removeExtension('foo') - expect(BrowserWindow.getExtensions()).to.not.have.property('foo') - - const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo') - BrowserWindow.addExtension(extensionPath) - expect(BrowserWindow.getExtensions()).to.have.property('foo') - - showLastDevToolsPanel() - - w.loadURL('about:blank') - }) - - it('throws errors for missing manifest.json files', () => { - expect(() => { - BrowserWindow.addExtension(path.join(__dirname, 'does-not-exist')) - }).to.throw('ENOENT: no such file or directory') - }) - - it('throws errors for invalid manifest.json files', () => { - expect(() => { - BrowserWindow.addExtension(path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest')) - }).to.throw('Unexpected token }') - }) - }) - }) - - describe('window.webContents.executeJavaScript', () => { - const expected = 'hello, world!' - const expectedErrorMsg = 'woops!' - const code = `(() => "${expected}")()` - const asyncCode = `(() => new Promise(r => setTimeout(() => r("${expected}"), 500)))()` - const badAsyncCode = `(() => new Promise((r, e) => setTimeout(() => e("${expectedErrorMsg}"), 500)))()` - const errorTypes = new Set([ - Error, - ReferenceError, - EvalError, - RangeError, - SyntaxError, - TypeError, - URIError - ]) - - it('resolves the returned promise with the result', (done) => { - ipcRenderer.send('executeJavaScript', code) - ipcRenderer.once('executeJavaScript-promise-response', (event, result) => { - expect(result).to.equal(expected) - done() - }) - }) - it('resolves the returned promise with the result if the code returns an asyncronous promise', (done) => { - ipcRenderer.send('executeJavaScript', asyncCode) - ipcRenderer.once('executeJavaScript-promise-response', (event, result) => { - expect(result).to.equal(expected) - done() - }) - }) - it('rejects the returned promise if an async error is thrown', (done) => { - ipcRenderer.send('executeJavaScript', badAsyncCode) - ipcRenderer.once('executeJavaScript-promise-error', (event, error) => { - expect(error).to.equal(expectedErrorMsg) - done() - }) - }) - it('rejects the returned promise with an error if an Error.prototype is thrown', async () => { - for (const error in errorTypes) { - await new Promise((resolve) => { - ipcRenderer.send('executeJavaScript', `Promise.reject(new ${error.name}("Wamp-wamp")`) - ipcRenderer.once('executeJavaScript-promise-error-name', (event, name) => { - expect(name).to.equal(error.name) - resolve() - }) - }) - } - }) - - it('works after page load and during subframe load', (done) => { - w.webContents.once('did-finish-load', () => { - // initiate a sub-frame load, then try and execute script during it - w.webContents.executeJavaScript(` - var iframe = document.createElement('iframe') - iframe.src = '${server.url}/slow' - document.body.appendChild(iframe) - `).then(() => { - w.webContents.executeJavaScript('console.log(\'hello\')').then(() => { - done() - }) - }) - }) - w.loadURL(server.url) - }) - - it('executes after page load', (done) => { - w.webContents.executeJavaScript(code).then(result => { - expect(result).to.equal(expected) - done() - }) - w.loadURL(server.url) - }) - - it('works with result objects that have DOM class prototypes', (done) => { - w.webContents.executeJavaScript('document.location').then(result => { - expect(result.origin).to.equal(server.url) - expect(result.protocol).to.equal('http:') - done() - }) - w.loadURL(server.url) - }) - }) - - describe('previewFile', () => { - before(function () { - if (process.platform !== 'darwin') { - this.skip() - } - }) - - it('opens the path in Quick Look on macOS', () => { - expect(() => { - w.previewFile(__filename) - w.closeFilePreview() - }).to.not.throw() - }) - }) - - describe('contextIsolation option with and without sandbox option', () => { - const expectedContextData = { - preloadContext: { - preloadProperty: 'number', - pageProperty: 'undefined', - typeofRequire: 'function', - typeofProcess: 'object', - typeofArrayPush: 'function', - typeofFunctionApply: 'function', - typeofPreloadExecuteJavaScriptProperty: 'undefined' - }, - pageContext: { - preloadProperty: 'undefined', - pageProperty: 'string', - typeofRequire: 'undefined', - typeofProcess: 'undefined', - typeofArrayPush: 'number', - typeofFunctionApply: 'boolean', - typeofPreloadExecuteJavaScriptProperty: 'number', - typeofOpenedWindow: 'object' - } - } - - beforeEach(() => { - if (iw != null) iw.destroy() - iw = new BrowserWindow({ - show: false, - webPreferences: { - contextIsolation: true, - preload: path.join(fixtures, 'api', 'isolated-preload.js') - } - }) - if (ws != null) ws.destroy() - ws = new BrowserWindow({ - show: false, - webPreferences: { - sandbox: true, - contextIsolation: true, - preload: path.join(fixtures, 'api', 'isolated-preload.js') - } - }) - }) - - afterEach(() => { - if (iw != null) iw.destroy() - if (ws != null) ws.destroy() - }) - - it('separates the page context from the Electron/preload context', async () => { - const p = emittedOnce(ipcMain, 'isolated-world') - iw.loadFile(path.join(fixtures, 'api', 'isolated.html')) - const [, data] = await p - expect(data).to.deep.equal(expectedContextData) - }) - it('recreates the contexts on reload', async () => { - await iw.loadFile(path.join(fixtures, 'api', 'isolated.html')) - const isolatedWorld = emittedOnce(ipcMain, 'isolated-world') - iw.webContents.reload() - const [, data] = await isolatedWorld - expect(data).to.deep.equal(expectedContextData) - }) - it('enables context isolation on child windows', async () => { - const browserWindowCreated = emittedOnce(app, 'browser-window-created') - iw.loadFile(path.join(fixtures, 'pages', 'window-open.html')) - const [, window] = await browserWindowCreated - expect(window.webContents.getLastWebPreferences().contextIsolation).to.be.true() - }) - it('separates the page context from the Electron/preload context with sandbox on', async () => { - const p = emittedOnce(ipcMain, 'isolated-world') - ws.loadFile(path.join(fixtures, 'api', 'isolated.html')) - const [, data] = await p - expect(data).to.deep.equal(expectedContextData) - }) - it('recreates the contexts on reload with sandbox on', async () => { - await ws.loadFile(path.join(fixtures, 'api', 'isolated.html')) - const isolatedWorld = emittedOnce(ipcMain, 'isolated-world') - ws.webContents.reload() - const [, data] = await isolatedWorld - expect(data).to.deep.equal(expectedContextData) - }) - it('supports fetch api', async () => { - const fetchWindow = new BrowserWindow({ - show: false, - webPreferences: { - contextIsolation: true, - preload: path.join(fixtures, 'api', 'isolated-fetch-preload.js') - } - }) - const p = emittedOnce(ipcMain, 'isolated-fetch-error') - fetchWindow.loadURL('about:blank') - const [, error] = await p - fetchWindow.destroy() - expect(error).to.equal('Failed to fetch') - }) - it('doesn\'t break ipc serialization', async () => { - const p = emittedOnce(ipcMain, 'isolated-world') - iw.loadURL('about:blank') - iw.webContents.executeJavaScript(` - const opened = window.open() - openedLocation = opened.location.href - opened.close() - window.postMessage({openedLocation}, '*') - `) - const [, data] = await p - expect(data.pageContext.openedLocation).to.equal('') - }) - }) - - describe('offscreen rendering', () => { - beforeEach(function () { - if (!features.isOffscreenRenderingEnabled()) { - // XXX(alexeykuzmin): "afterEach" hook is not called - // for skipped tests, we have to close the window manually. - return closeTheWindow().then(() => { this.skip() }) - } - - if (w != null) w.destroy() - w = new BrowserWindow({ - width: 100, - height: 100, - show: false, - webPreferences: { - backgroundThrottling: false, - offscreen: true - } - }) - }) - - it('creates offscreen window with correct size', (done) => { - w.webContents.once('paint', function (event, rect, data) { - expect(data.constructor.name).to.equal('NativeImage') - expect(data.isEmpty()).to.be.false() - const size = data.getSize() - expect(size.width).to.be.closeTo(100 * devicePixelRatio, 2) - expect(size.height).to.be.closeTo(100 * devicePixelRatio, 2) - done() - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - - it('does not crash after navigation', () => { - w.webContents.loadURL('about:blank') - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - - describe('window.webContents.isOffscreen()', () => { - it('is true for offscreen type', () => { - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - expect(w.webContents.isOffscreen()).to.be.true() - }) - - it('is false for regular window', () => { - const c = new BrowserWindow({ show: false }) - expect(c.webContents.isOffscreen()).to.be.false() - c.destroy() - }) - }) - - describe('window.webContents.isPainting()', () => { - it('returns whether is currently painting', (done) => { - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.isPainting()).to.be.true() - done() - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - - describe('window.webContents.stopPainting()', () => { - it('stops painting', (done) => { - w.webContents.on('dom-ready', () => { - w.webContents.stopPainting() - expect(w.webContents.isPainting()).to.be.false() - done() - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - - describe('window.webContents.startPainting()', () => { - it('starts painting', (done) => { - w.webContents.on('dom-ready', () => { - w.webContents.stopPainting() - w.webContents.startPainting() - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.isPainting()).to.be.true() - done() - }) - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - - // TODO(codebytere): remove in Electron v8.0.0 - describe('window.webContents.getFrameRate()', () => { - it('has default frame rate', (done) => { - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.getFrameRate()).to.equal(60) - done() - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - - // TODO(codebytere): remove in Electron v8.0.0 - describe('window.webContents.setFrameRate(frameRate)', () => { - it('sets custom frame rate', (done) => { - w.webContents.on('dom-ready', () => { - w.webContents.setFrameRate(30) - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.getFrameRate()).to.equal(30) - done() - }) - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - - describe('window.webContents.FrameRate', () => { - it('has default frame rate', (done) => { - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.frameRate).to.equal(60) - done() - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - - it('sets custom frame rate', (done) => { - w.webContents.on('dom-ready', () => { - w.webContents.frameRate = 30 - w.webContents.once('paint', function (event, rect, data) { - expect(w.webContents.frameRate).to.equal(30) - done() - }) - }) - w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html')) - }) - }) - }) -}) - -const expectBoundsEqual = (actual, expected) => { - if (!isScaleFactorRounding()) { - expect(expected).to.deep.equal(actual) - } else if (Array.isArray(actual)) { - expect(actual[0]).to.be.closeTo(expected[0], 1) - expect(actual[1]).to.be.closeTo(expected[1], 1) - } else { - expect(actual.x).to.be.closeTo(expected.x, 1) - expect(actual.y).to.be.closeTo(expected.y, 1) - expect(actual.width).to.be.closeTo(expected.width, 1) - expect(actual.height).to.be.closeTo(expected.height, 1) - } -} - -// Is the display's scale factor possibly causing rounding of pixel coordinate -// values? -const isScaleFactorRounding = () => { - const { scaleFactor } = screen.getPrimaryDisplay() - // Return true if scale factor is non-integer value - if (Math.round(scaleFactor) !== scaleFactor) return true - // Return true if scale factor is odd number above 2 - return scaleFactor > 2 && scaleFactor % 2 === 1 -} diff --git a/spec/static/main.js b/spec/static/main.js index 99eb419d5ed1..b463a39440bf 100644 --- a/spec/static/main.js +++ b/spec/static/main.js @@ -159,18 +159,6 @@ app.on('ready', function () { }) event.returnValue = null }) - - ipcMain.on('executeJavaScript', function (event, code) { - window.webContents.executeJavaScript(code).then((result) => { - window.webContents.send('executeJavaScript-promise-response', result) - }).catch((error) => { - window.webContents.send('executeJavaScript-promise-error', error) - - if (error && error.name) { - window.webContents.send('executeJavaScript-promise-error-name', error.name) - } - }) - }) }) ipcMain.on('handle-next-ipc-message-sync', function (event, returnValue) { @@ -224,18 +212,10 @@ ipcMain.on('create-window-with-options-cycle', (event) => { event.returnValue = window.id }) -ipcMain.on('prevent-next-new-window', (event, id) => { - webContents.fromId(id).once('new-window', event => event.preventDefault()) -}) - ipcMain.on('prevent-next-will-attach-webview', (event) => { event.sender.once('will-attach-webview', event => event.preventDefault()) }) -ipcMain.on('prevent-next-will-prevent-unload', (event, id) => { - webContents.fromId(id).once('will-prevent-unload', event => event.preventDefault()) -}) - ipcMain.on('disable-node-on-next-will-attach-webview', (event, id) => { event.sender.once('will-attach-webview', (event, webPreferences, params) => { params.src = `file://${path.join(__dirname, '..', 'fixtures', 'pages', 'c.html')}`