diff --git a/docs/api/structures/input-event.md b/docs/api/structures/input-event.md index c24cb528a79..c7bcb2f4b36 100644 --- a/docs/api/structures/input-event.md +++ b/docs/api/structures/input-event.md @@ -1,6 +1,6 @@ # InputEvent Object -* `modifiers` String[] - An array of modifiers of the event, can - be `shift`, `control`, `alt`, `meta`, `isKeypad`, `isAutoRepeat`, - `leftButtonDown`, `middleButtonDown`, `rightButtonDown`, `capsLock`, - `numLock`, `left`, `right`. +* `modifiers` String[] (optional) - An array of modifiers of the event, can + be `shift`, `control`, `ctrl`, `alt`, `meta`, `command`, `cmd`, `isKeypad`, + `isAutoRepeat`, `leftButtonDown`, `middleButtonDown`, `rightButtonDown`, + `capsLock`, `numLock`, `left`, `right`. diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index f5d25bac825..52a1a39b886 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1593,8 +1593,8 @@ End subscribing for frame presentation events. * `item` Object * `file` String[] | String - The path(s) to the file(s) being dragged. - * `icon` [NativeImage](native-image.md) - The image must be non-empty on - macOS. + * `icon` [NativeImage](native-image.md) | String - The image must be + non-empty on macOS. Sets the `item` as dragging item for current drag-drop operation, `file` is the absolute path of the file to be dragged, and `icon` is the image showing under diff --git a/script/codesign/import-testing-cert-ci.sh b/script/codesign/import-testing-cert-ci.sh index 3772373015f..03732f89d6e 100755 --- a/script/codesign/import-testing-cert-ci.sh +++ b/script/codesign/import-testing-cert-ci.sh @@ -7,8 +7,6 @@ security create-keychain -p $KEYCHAIN_PASSWORD $KEY_CHAIN security default-keychain -s $KEY_CHAIN # Unlock the keychain security unlock-keychain -p $KEYCHAIN_PASSWORD $KEY_CHAIN -# Set keychain locking timeout to 3600 seconds -security set-keychain-settings -t 3600 -u $KEY_CHAIN # Add certificates to keychain and allow codesign to access them security import "$(dirname $0)"/signing.cer -k $KEY_CHAIN -A /usr/bin/codesign @@ -16,7 +14,7 @@ security import "$(dirname $0)"/signing.pem -k $KEY_CHAIN -A /usr/bin/codesign security import "$(dirname $0)"/signing.p12 -k $KEY_CHAIN -P $SPEC_KEY_PASSWORD -A /usr/bin/codesign echo "Add keychain to keychain-list" -security list-keychains -s mac-build.keychain +security list-keychains -s $KEY_CHAIN echo "Setting key partition list" security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASSWORD $KEY_CHAIN diff --git a/spec-main/ambient.d.ts b/spec-main/ambient.d.ts index 10f112f86af..bb3b467b5b0 100644 --- a/spec-main/ambient.d.ts +++ b/spec-main/ambient.d.ts @@ -16,6 +16,7 @@ declare namespace Electron { interface WebContents { getOwnerBrowserWindow(): BrowserWindow; + getWebPreferences(): any; } interface Session { diff --git a/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index 31bff4b866d..989b4b9c971 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -3,9 +3,10 @@ import { AddressInfo } from 'net' import * as chaiAsPromised from 'chai-as-promised' import * as path from 'path' import * as http from 'http' -import { BrowserWindow, ipcMain, webContents } from 'electron' +import { BrowserWindow, ipcMain, webContents, session } from 'electron' import { emittedOnce } from './events-helpers'; import { closeAllWindows } from './window-helpers'; +import { ifdescribe } from './spec-helpers'; const { expect } = chai @@ -100,6 +101,7 @@ describe('webContents module', () => { }) describe('webContents.print()', () => { + afterEach(closeAllWindows) it('throws when invalid settings are passed', () => { const w = new BrowserWindow({ show: false }) expect(() => { @@ -221,4 +223,727 @@ describe('webContents module', () => { }) }) }) + + describe('loadURL() promise API', () => { + let w: BrowserWindow + beforeEach(async () => { + w = new BrowserWindow({show: false}) + }) + afterEach(closeAllWindows) + + it('resolves when done loading', async () => { + await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled() + }) + + it('resolves when done loading a file URL', async () => { + await expect(w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html'))).to.eventually.be.fulfilled() + }) + + it('rejects when failing to load a file URL', async () => { + await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected() + .and.have.property('code', 'ERR_FILE_NOT_FOUND') + }) + + it('rejects when loading fails due to DNS not resolved', async () => { + await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected() + .and.have.property('code', 'ERR_NAME_NOT_RESOLVED') + }) + + it('rejects when navigation is cancelled due to a bad scheme', async () => { + await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected() + .and.have.property('code', 'ERR_FAILED') + }) + + it('sets appropriate error information on rejection', async () => { + let err + try { + await w.loadURL('file:non-existent') + } catch (e) { + err = e + } + expect(err).not.to.be.null() + expect(err.code).to.eql('ERR_FILE_NOT_FOUND') + expect(err.errno).to.eql(-6) + expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent') + }) + + it('rejects if the load is aborted', async () => { + const s = http.createServer((req, res) => { /* never complete the request */ }) + await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) + const { port } = s.address() as AddressInfo + const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/) + // load a different file before the first load completes, causing the + // first load to be aborted. + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')) + await p + s.close() + }) + + it("doesn't reject when a subframe fails to load", async () => { + let resp = null as unknown as http.ServerResponse + const s = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.write('') + resp = res + // don't end the response yet + }) + await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) + const { port } = s.address() as AddressInfo + const p = new Promise(resolve => { + w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { + if (!isMainFrame) { + resolve() + } + }) + }) + const main = w.loadURL(`http://127.0.0.1:${port}`) + await p + resp.end() + await main + s.close() + }) + + it("doesn't resolve when a subframe loads", async () => { + let resp = null as unknown as http.ServerResponse + const s = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.write('') + resp = res + // don't end the response yet + }) + await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) + const { port } = s.address() as AddressInfo + const p = new Promise(resolve => { + w.webContents.on('did-frame-finish-load', (event, isMainFrame, frameProcessId, frameRoutingId) => { + if (!isMainFrame) { + resolve() + } + }) + }) + const main = w.loadURL(`http://127.0.0.1:${port}`) + await p + resp.destroy() // cause the main request to fail + await expect(main).to.eventually.be.rejected() + .and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING + s.close() + }) + }) + + describe('getFocusedWebContents() API', () => { + afterEach(closeAllWindows) + it('returns the focused web contents', async () => { + const w = new BrowserWindow({show: true}) + await w.loadURL('about:blank') + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id) + + const devToolsOpened = emittedOnce(w.webContents, 'devtools-opened') + w.webContents.openDevTools() + await devToolsOpened + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.devToolsWebContents.id) + const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed') + w.webContents.closeDevTools() + await devToolsClosed + expect(webContents.getFocusedWebContents().id).to.equal(w.webContents.id) + }) + + it('does not crash when called on a detached dev tools window', async () => { + const w = new BrowserWindow({show: true}) + + w.webContents.openDevTools({ mode: 'detach' }) + w.webContents.inspectElement(100, 100) + + // For some reason we have to wait for two focused events...? + await emittedOnce(w.webContents, 'devtools-focused') + await emittedOnce(w.webContents, 'devtools-focused') + + expect(() => { webContents.getFocusedWebContents() }).to.not.throw() + + // Work around https://github.com/electron/electron/issues/19985 + await new Promise(r => setTimeout(r, 0)) + + const devToolsClosed = emittedOnce(w.webContents, 'devtools-closed') + w.webContents.closeDevTools() + await devToolsClosed + expect(() => { webContents.getFocusedWebContents() }).to.not.throw() + }) + }) + + describe('setDevToolsWebContents() API', () => { + afterEach(closeAllWindows) + it('sets arbitrary webContents as devtools', async () => { + const w = new BrowserWindow({ show: false }) + const devtools = new BrowserWindow({ show: false }) + const promise = emittedOnce(devtools.webContents, 'dom-ready') + w.webContents.setDevToolsWebContents(devtools.webContents) + w.webContents.openDevTools() + await promise + expect(devtools.webContents.getURL().startsWith('devtools://devtools')).to.be.true() + const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name') + expect(result).to.equal('InspectorFrontendHostImpl') + devtools.destroy() + }) + }) + + describe('isFocused() API', () => { + it('returns false when the window is hidden', async () => { + const w = new BrowserWindow({ show: false }) + await w.loadURL('about:blank') + expect(w.isVisible()).to.be.false() + expect(w.webContents.isFocused()).to.be.false() + }) + }) + + describe('isCurrentlyAudible() API', () => { + afterEach(closeAllWindows) + it('returns whether audio is playing', async () => { + const w = new BrowserWindow({ show: false }) + await w.loadURL('about:blank') + await w.webContents.executeJavaScript(` + window.context = new AudioContext + // Start in suspended state, because of the + // new web audio api policy. + context.suspend() + window.oscillator = context.createOscillator() + oscillator.connect(context.destination) + oscillator.start() + `) + let p = emittedOnce(w.webContents, '-audio-state-changed') + w.webContents.executeJavaScript('context.resume()') + await p + expect(w.webContents.isCurrentlyAudible()).to.be.true() + p = emittedOnce(w.webContents, '-audio-state-changed') + w.webContents.executeJavaScript('oscillator.stop()') + await p + expect(w.webContents.isCurrentlyAudible()).to.be.false() + }) + }) + + describe('getWebPreferences() API', () => { + afterEach(closeAllWindows) + it('should not crash when called for devTools webContents', (done) => { + const w = new BrowserWindow({show: false}) + w.webContents.openDevTools() + w.webContents.once('devtools-opened', () => { + expect(w.webContents.devToolsWebContents.getWebPreferences()).to.be.null() + done() + }) + }) + }) + + describe('openDevTools() API', () => { + afterEach(closeAllWindows) + it('can show window with activation', async () => { + const w = new BrowserWindow({show: false}) + const focused = emittedOnce(w, 'focus') + w.show() + await focused + expect(w.isFocused()).to.be.true() + w.webContents.openDevTools({ mode: 'detach', activate: true }) + await emittedOnce(w.webContents, 'devtools-focused') + await emittedOnce(w.webContents, 'devtools-focused') + await emittedOnce(w.webContents, 'devtools-opened') + await new Promise(resolve => setTimeout(resolve, 0)) + expect(w.isFocused()).to.be.false() + }) + + it('can show window without activation', async () => { + const w = new BrowserWindow({show: false}) + const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened') + w.webContents.openDevTools({ mode: 'detach', activate: false }) + await devtoolsOpened + expect(w.webContents.isDevToolsOpened()).to.be.true() + }) + }) + + describe('before-input-event event', () => { + afterEach(closeAllWindows) + it('can prevent document keyboard events', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html')) + const keyDown = new Promise(resolve => { + ipcMain.once('keydown', (event, key) => resolve(key)) + }) + w.webContents.once('before-input-event', (event, input) => { + if ('a' === input.key) event.preventDefault() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' }) + expect(await keyDown).to.equal('b') + }) + + it('has the correct properties', async () => { + const w = new BrowserWindow({show: false}) + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')) + const testBeforeInput = async (opts: any) => { + const modifiers = [] + if (opts.shift) modifiers.push('shift') + if (opts.control) modifiers.push('control') + if (opts.alt) modifiers.push('alt') + if (opts.meta) modifiers.push('meta') + if (opts.isAutoRepeat) modifiers.push('isAutoRepeat') + + const p = emittedOnce(w.webContents, 'before-input-event') + w.webContents.sendInputEvent({ + type: opts.type, + keyCode: opts.keyCode, + modifiers: modifiers as any + }) + const [, input] = await p + + expect(input.type).to.equal(opts.type) + expect(input.key).to.equal(opts.key) + expect(input.code).to.equal(opts.code) + expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat) + expect(input.shift).to.equal(opts.shift) + expect(input.control).to.equal(opts.control) + expect(input.alt).to.equal(opts.alt) + expect(input.meta).to.equal(opts.meta) + } + await testBeforeInput({ + type: 'keyDown', + key: 'A', + code: 'KeyA', + keyCode: 'a', + shift: true, + control: true, + alt: true, + meta: true, + isAutoRepeat: true + }) + await testBeforeInput({ + type: 'keyUp', + key: '.', + code: 'Period', + keyCode: '.', + shift: false, + control: true, + alt: true, + meta: false, + isAutoRepeat: false + }) + await testBeforeInput({ + type: 'keyUp', + key: '!', + code: 'Digit1', + keyCode: '1', + shift: true, + control: false, + alt: false, + meta: true, + isAutoRepeat: false + }) + await testBeforeInput({ + type: 'keyUp', + key: 'Tab', + code: 'Tab', + keyCode: 'Tab', + shift: false, + control: true, + alt: false, + meta: false, + isAutoRepeat: true + }) + }) + }) + + // On Mac, zooming isn't done with the mouse wheel. + ifdescribe(process.platform !== 'darwin')('zoom-changed', () => { + afterEach(closeAllWindows) + it('is emitted with the correct zooming info', async () => { + const w = new BrowserWindow({ show: false }) + await w.loadFile(path.join(fixturesPath, 'pages', 'base-page.html')) + + const testZoomChanged = async ({ zoomingIn }: { zoomingIn: boolean }) => { + w.webContents.sendInputEvent({ + type: 'mouseWheel', + x: 300, + y: 300, + deltaX: 0, + deltaY: zoomingIn ? 1 : -1, + wheelTicksX: 0, + wheelTicksY: zoomingIn ? 1 : -1, + modifiers: ['control', 'meta'] + }) + + const [, zoomDirection] = await emittedOnce(w.webContents, 'zoom-changed') + expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out') + // Apparently we get two zoom-changed events?? + await emittedOnce(w.webContents, 'zoom-changed') + } + + await testZoomChanged({ zoomingIn: true }) + await testZoomChanged({ zoomingIn: false }) + }) + }) + + describe('sendInputEvent(event)', () => { + let w: BrowserWindow + beforeEach(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + await w.loadFile(path.join(fixturesPath, 'pages', 'key-events.html')) + }) + afterEach(closeAllWindows) + + it('can send keydown events', (done) => { + ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { + expect(key).to.equal('a') + expect(code).to.equal('KeyA') + expect(keyCode).to.equal(65) + expect(shiftKey).to.be.false() + expect(ctrlKey).to.be.false() + expect(altKey).to.be.false() + done() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) + }) + + it('can send keydown events with modifiers', (done) => { + ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { + expect(key).to.equal('Z') + expect(code).to.equal('KeyZ') + expect(keyCode).to.equal(90) + expect(shiftKey).to.be.true() + expect(ctrlKey).to.be.true() + expect(altKey).to.be.false() + done() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) + }) + + it('can send keydown events with special keys', (done) => { + ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { + expect(key).to.equal('Tab') + expect(code).to.equal('Tab') + expect(keyCode).to.equal(9) + expect(shiftKey).to.be.false() + expect(ctrlKey).to.be.false() + expect(altKey).to.be.true() + done() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] }) + }) + + it('can send char events', (done) => { + ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { + expect(key).to.equal('a') + expect(code).to.equal('KeyA') + expect(keyCode).to.equal(65) + expect(shiftKey).to.be.false() + expect(ctrlKey).to.be.false() + expect(altKey).to.be.false() + done() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) + w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' }) + }) + + it('can send char events with modifiers', (done) => { + ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { + expect(key).to.equal('Z') + expect(code).to.equal('KeyZ') + expect(keyCode).to.equal(90) + expect(shiftKey).to.be.true() + expect(ctrlKey).to.be.true() + expect(altKey).to.be.false() + done() + }) + w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' }) + w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) + }) + }) + + describe('insertCSS', () => { + afterEach(closeAllWindows) + it('supports inserting CSS', async () => { + const w = new BrowserWindow({ show: false }) + w.loadURL('about:blank') + await w.webContents.insertCSS('body { background-repeat: round; }') + const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') + expect(result).to.equal('round') + }) + + it('supports removing inserted CSS', async () => { + const w = new BrowserWindow({ show: false }) + w.loadURL('about:blank') + const key = await w.webContents.insertCSS('body { background-repeat: round; }') + await w.webContents.removeInsertedCSS(key) + const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') + expect(result).to.equal('repeat') + }) + }) + + describe('inspectElement()', () => { + afterEach(closeAllWindows) + it('supports inspecting an element in the devtools', (done) => { + const w = new BrowserWindow({ show: false }) + w.loadURL('about:blank') + w.webContents.once('devtools-opened', () => { done() }) + w.webContents.inspectElement(10, 10) + }) + }) + + describe('startDrag({file, icon})', () => { + it('throws errors for a missing file or a missing/empty icon', () => { + const w = new BrowserWindow({ show: false }) + expect(() => { + w.webContents.startDrag({ icon: path.join(fixturesPath, 'assets', 'logo.png') } as any) + }).to.throw(`Must specify either 'file' or 'files' option`) + + expect(() => { + w.webContents.startDrag({ file: __filename } as any) + }).to.throw(`Must specify 'icon' option`) + + if (process.platform === 'darwin') { + expect(() => { + w.webContents.startDrag({ file: __filename, icon: __filename }) + }).to.throw(`Must specify non-empty 'icon' option`) + } + }) + }) + + describe('focus()', () => { + describe('when the web contents is hidden', () => { + afterEach(closeAllWindows) + it('does not blur the focused window', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + ipcMain.once('answer', (event, parentFocused, childFocused) => { + expect(parentFocused).to.be.true() + expect(childFocused).to.be.false() + done() + }) + w.show() + w.loadFile(path.join(fixturesPath, 'pages', 'focus-web-contents.html')) + }) + }) + }) + + describe('getOSProcessId()', () => { + afterEach(closeAllWindows) + it('returns a valid procress id', async () => { + const w = new BrowserWindow({ show: false }) + expect(w.webContents.getOSProcessId()).to.equal(0) + + await w.loadURL('about:blank') + expect(w.webContents.getOSProcessId()).to.be.above(0) + }) + }) + + describe('zoom api', () => { + const scheme = (global as any).standardScheme + const hostZoomMap: Record = { + host1: 0.3, + host2: 0.7, + host3: 0.2 + } + + before((done) => { + const protocol = session.defaultSession.protocol + protocol.registerStringProtocol(scheme, (request, callback) => { + const response = `` + callback({ data: response, mimeType: 'text/html' }) + }, (error) => done(error)) + }) + + after((done) => { + const protocol = session.defaultSession.protocol + protocol.unregisterProtocol(scheme, (error) => done(error)) + }) + + afterEach(closeAllWindows) + + // TODO(codebytere): remove in Electron v8.0.0 + it('can set the correct zoom level (functions)', async () => { + const w = new BrowserWindow({ show: false }) + try { + await w.loadURL('about:blank') + const zoomLevel = w.webContents.getZoomLevel() + expect(zoomLevel).to.eql(0.0) + w.webContents.setZoomLevel(0.5) + const newZoomLevel = w.webContents.getZoomLevel() + expect(newZoomLevel).to.eql(0.5) + } finally { + w.webContents.setZoomLevel(0) + } + }) + + it('can set the correct zoom level', async () => { + const w = new BrowserWindow({ show: false }) + try { + await w.loadURL('about:blank') + const zoomLevel = w.webContents.zoomLevel + expect(zoomLevel).to.eql(0.0) + w.webContents.zoomLevel = 0.5 + const newZoomLevel = w.webContents.zoomLevel + expect(newZoomLevel).to.eql(0.5) + } finally { + w.webContents.zoomLevel = 0 + } + }) + + it('can persist zoom level across navigation', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + let finalNavigation = false + ipcMain.on('set-zoom', (e, host) => { + const zoomLevel = hostZoomMap[host] + if (!finalNavigation) w.webContents.zoomLevel = zoomLevel + e.sender.send(`${host}-zoom-set`) + }) + ipcMain.on('host1-zoom-level', (e, zoomLevel) => { + const expectedZoomLevel = hostZoomMap.host1 + expect(zoomLevel).to.equal(expectedZoomLevel) + if (finalNavigation) { + done() + } else { + w.loadURL(`${scheme}://host2`) + } + }) + ipcMain.once('host2-zoom-level', (e, zoomLevel) => { + const expectedZoomLevel = hostZoomMap.host2 + expect(zoomLevel).to.equal(expectedZoomLevel) + finalNavigation = true + w.webContents.goBack() + }) + w.loadURL(`${scheme}://host1`) + }) + + it('can propagate zoom level across same session', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + const w2 = new BrowserWindow({ show: false }) + w2.webContents.on('did-finish-load', () => { + const zoomLevel1 = w.webContents.zoomLevel + expect(zoomLevel1).to.equal(hostZoomMap.host3) + + const zoomLevel2 = w2.webContents.zoomLevel + expect(zoomLevel1).to.equal(zoomLevel2) + w2.setClosable(true) + w2.close() + done() + }) + w.webContents.on('did-finish-load', () => { + w.webContents.zoomLevel = hostZoomMap.host3 + w2.loadURL(`${scheme}://host3`) + }) + w.loadURL(`${scheme}://host3`) + }) + + it('cannot propagate zoom level across different session', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + const w2 = new BrowserWindow({ + show: false, + webPreferences: { + partition: 'temp' + } + }) + const protocol = w2.webContents.session.protocol + protocol.registerStringProtocol(scheme, (request, callback) => { + callback('hello') + }, (error) => { + if (error) return done(error) + w2.webContents.on('did-finish-load', () => { + const zoomLevel1 = w.webContents.zoomLevel + expect(zoomLevel1).to.equal(hostZoomMap.host3) + + const zoomLevel2 = w2.webContents.zoomLevel + expect(zoomLevel2).to.equal(0) + expect(zoomLevel1).to.not.equal(zoomLevel2) + + protocol.unregisterProtocol(scheme, (error) => { + if (error) return done(error) + w2.setClosable(true) + w2.close() + done() + }) + }) + w.webContents.on('did-finish-load', () => { + w.webContents.zoomLevel = hostZoomMap.host3 + w2.loadURL(`${scheme}://host3`) + }) + w.loadURL(`${scheme}://host3`) + }) + }) + + it('can persist when it contains iframe', (done) => { + const w = new BrowserWindow({ show: false }) + const server = http.createServer((req, res) => { + setTimeout(() => { + res.end() + }, 200) + }) + server.listen(0, '127.0.0.1', () => { + const url = 'http://127.0.0.1:' + (server.address() as AddressInfo).port + const content = `` + w.webContents.on('did-frame-finish-load', (e, isMainFrame) => { + if (!isMainFrame) { + const zoomLevel = w.webContents.zoomLevel + expect(zoomLevel).to.equal(2.0) + + w.webContents.zoomLevel = 0 + server.close() + done() + } + }) + w.webContents.on('dom-ready', () => { + w.webContents.zoomLevel = 2.0 + }) + w.loadURL(`data:text/html,${content}`) + }) + }) + + it('cannot propagate when used with webframe', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + let finalZoomLevel = 0 + const w2 = new BrowserWindow({ + show: false + }) + w2.webContents.on('did-finish-load', () => { + const zoomLevel1 = w.webContents.zoomLevel + expect(zoomLevel1).to.equal(finalZoomLevel) + + const zoomLevel2 = w2.webContents.zoomLevel + expect(zoomLevel2).to.equal(0) + expect(zoomLevel1).to.not.equal(zoomLevel2) + + w2.setClosable(true) + w2.close() + done() + }) + ipcMain.once('temporary-zoom-set', (e, zoomLevel) => { + w2.loadFile(path.join(fixturesPath, 'pages', 'c.html')) + finalZoomLevel = zoomLevel + }) + w.loadFile(path.join(fixturesPath, 'pages', 'webframe-zoom.html')) + }) + + it('cannot persist zoom level after navigation with webFrame', (done) => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + let initialNavigation = true + const source = ` + const {ipcRenderer, webFrame} = require('electron') + webFrame.setZoomLevel(0.6) + ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel()) + ` + w.webContents.on('did-finish-load', () => { + if (initialNavigation) { + w.webContents.executeJavaScript(source) + } else { + const zoomLevel = w.webContents.zoomLevel + expect(zoomLevel).to.equal(0) + done() + } + }) + ipcMain.once('zoom-level-set', (e, zoomLevel) => { + expect(zoomLevel).to.equal(0.6) + w.loadFile(path.join(fixturesPath, 'pages', 'd.html')) + initialNavigation = false + }) + w.loadFile(path.join(fixturesPath, 'pages', 'c.html')) + }) + }) }) diff --git a/spec-main/index.js b/spec-main/index.js index 1ad1442bbe3..772f66ec26c 100644 --- a/spec-main/index.js +++ b/spec-main/index.js @@ -25,8 +25,10 @@ app.on('window-all-closed', () => null) app.commandLine.appendSwitch('ignore-certificate-errors') global.standardScheme = 'app' +global.zoomScheme = 'zoom' protocol.registerSchemesAsPrivileged([ { scheme: global.standardScheme, privileges: { standard: true, secure: true } }, + { scheme: global.zoomScheme, privileges: { standard: true, secure: true } }, { scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } }, { scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } }, { scheme: 'no-cors', privileges: { supportFetchAPI: true } }, diff --git a/spec/api-shell-spec.js b/spec/api-shell-spec.js index 346d3f9521b..11f266c5ffc 100644 --- a/spec/api-shell-spec.js +++ b/spec/api-shell-spec.js @@ -4,10 +4,12 @@ const dirtyChai = require('dirty-chai') const fs = require('fs') const path = require('path') const os = require('os') +const http = require('http') const { shell, remote } = require('electron') const { BrowserWindow } = remote const { closeWindow } = require('./window-helpers') +const { emittedOnce } = require('./events-helpers') const { expect } = chai chai.use(dirtyChai) @@ -47,32 +49,34 @@ describe('shell module', () => { } }) - it('opens an external link', done => { - const url = 'http://www.example.com' + it('opens an external link', async () => { + let url = 'http://127.0.0.1' + let requestReceived if (process.platform === 'linux') { process.env.BROWSER = '/bin/true' process.env.DE = 'generic' process.env.DISPLAY = '' + requestReceived = Promise.resolve() + } else if (process.platform === 'darwin') { + // On the Mac CI machines, Safari tries to ask for a password to the + // code signing keychain we set up to test code signing (see + // https://github.com/electron/electron/pull/19969#issuecomment-526278890), + // so use a blur event as a crude proxy. + w = new BrowserWindow({ show: true }) + requestReceived = emittedOnce(w, 'blur') + } else { + const server = http.createServer((req, res) => { + res.end() + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + requestReceived = new Promise(resolve => server.on('connection', () => resolve())) + url = `http://127.0.0.1:${server.address().port}` } - // Ensure an external window is activated via a new window's blur event - w = new BrowserWindow() - let promiseResolved = false - let blurEventEmitted = false - - w.on('blur', () => { - blurEventEmitted = true - if (promiseResolved) { - done() - } - }) - - shell.openExternal(url).then(() => { - promiseResolved = true - if (blurEventEmitted || process.platform === 'linux') { - done() - } - }) + await Promise.all([ + shell.openExternal(url), + requestReceived + ]) }) }) diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 7c556d644c6..c2ff63135a8 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -40,347 +40,6 @@ describe('webContents module', () => { afterEach(() => closeWindow(w).then(() => { w = null })) - describe('loadURL() promise API', () => { - it('resolves when done loading', async () => { - await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled - }) - - it('resolves when done loading a file URL', async () => { - await expect(w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))).to.eventually.be.fulfilled - }) - - it('rejects when failing to load a file URL', async () => { - await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected - .and.have.property('code', 'ERR_FILE_NOT_FOUND') - }) - - it('rejects when loading fails due to DNS not resolved', async () => { - await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected - .and.have.property('code', 'ERR_NAME_NOT_RESOLVED') - }) - - it('rejects when navigation is cancelled due to a bad scheme', async () => { - await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected - .and.have.property('code', 'ERR_FAILED') - }) - - it('sets appropriate error information on rejection', async () => { - let err - try { - await w.loadURL('file:non-existent') - } catch (e) { - err = e - } - expect(err).not.to.be.null() - expect(err.code).to.eql('ERR_FILE_NOT_FOUND') - expect(err.errno).to.eql(-6) - expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent') - }) - - it('rejects if the load is aborted', async () => { - const s = http.createServer((req, res) => { /* never complete the request */ }) - await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) - const { port } = s.address() - const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/) - // load a different file before the first load completes, causing the - // first load to be aborted. - await w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - await p - s.close() - }) - - it("doesn't reject when a subframe fails to load", async () => { - let resp = null - const s = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.write('') - resp = res - // don't end the response yet - }) - await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) - const { port } = s.address() - const p = new Promise(resolve => { - w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { - if (!isMainFrame) { - resolve() - } - }) - }) - const main = w.loadURL(`http://127.0.0.1:${port}`) - await p - resp.end() - await main - s.close() - }) - - it("doesn't resolve when a subframe loads", async () => { - let resp = null - const s = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.write('') - resp = res - // don't end the response yet - }) - await new Promise(resolve => s.listen(0, '127.0.0.1', resolve)) - const { port } = s.address() - const p = new Promise(resolve => { - w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { - if (!isMainFrame) { - resolve() - } - }) - }) - const main = w.loadURL(`http://127.0.0.1:${port}`) - await p - resp.destroy() // cause the main request to fail - await expect(main).to.eventually.be.rejected - .and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING - s.close() - }) - }) - - describe('getFocusedWebContents() API', () => { - it('returns the focused web contents', (done) => { - if (isCi) return done() - - const specWebContents = remote.getCurrentWebContents() - expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id) - - specWebContents.once('devtools-opened', () => { - expect(specWebContents.devToolsWebContents.id).to.equal(webContents.getFocusedWebContents().id) - specWebContents.closeDevTools() - }) - - specWebContents.once('devtools-closed', () => { - expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id) - done() - }) - - specWebContents.openDevTools() - }) - - it('does not crash when called on a detached dev tools window', (done) => { - const specWebContents = w.webContents - - specWebContents.once('devtools-opened', () => { - expect(() => { - webContents.getFocusedWebContents() - }).to.not.throw() - specWebContents.closeDevTools() - }) - - specWebContents.once('devtools-closed', () => { - expect(() => { - webContents.getFocusedWebContents() - }).to.not.throw() - done() - }) - - specWebContents.openDevTools({ mode: 'detach' }) - w.inspectElement(100, 100) - }) - }) - - describe('setDevToolsWebContents() API', () => { - it('sets arbitrary webContents as devtools', async () => { - const devtools = new BrowserWindow({ show: false }) - const promise = emittedOnce(devtools.webContents, 'dom-ready') - w.webContents.setDevToolsWebContents(devtools.webContents) - w.webContents.openDevTools() - await promise - expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true() - const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name') - expect(result).to.equal('InspectorFrontendHostImpl') - devtools.destroy() - }) - }) - - describe('isFocused() API', () => { - it('returns false when the window is hidden', () => { - BrowserWindow.getAllWindows().forEach((window) => { - expect(!window.isVisible() && window.webContents.isFocused()).to.be.false() - }) - }) - }) - - describe('isCurrentlyAudible() API', () => { - it('returns whether audio is playing', async () => { - const webContents = remote.getCurrentWebContents() - const context = new window.AudioContext() - // Start in suspended state, because of the - // new web audio api policy. - context.suspend() - const oscillator = context.createOscillator() - oscillator.connect(context.destination) - oscillator.start() - let p = emittedOnce(webContents, '-audio-state-changed') - await context.resume() - await p - expect(webContents.isCurrentlyAudible()).to.be.true() - p = emittedOnce(webContents, '-audio-state-changed') - oscillator.stop() - await p - expect(webContents.isCurrentlyAudible()).to.be.false() - oscillator.disconnect() - context.close() - }) - }) - - describe('getWebPreferences() API', () => { - it('should not crash when called for devTools webContents', (done) => { - w.webContents.openDevTools() - w.webContents.once('devtools-opened', () => { - expect(w.devToolsWebContents.getWebPreferences()).to.be.null() - done() - }) - }) - }) - - describe('openDevTools() API', () => { - it('can show window with activation', async () => { - const focused = emittedOnce(w, 'focus') - w.show() - await focused - expect(w.isFocused()).to.be.true() - const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools({ mode: 'detach', activate: true }) - await devtoolsOpened - expect(w.isFocused()).to.be.false() - }) - - it('can show window without activation', async () => { - const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened') - w.webContents.openDevTools({ mode: 'detach', activate: false }) - await devtoolsOpened - expect(w.isDevToolsOpened()).to.be.true() - }) - }) - - describe('before-input-event event', () => { - it('can prevent document keyboard events', async () => { - await w.loadFile(path.join(fixtures, 'pages', 'key-events.html')) - const keyDown = new Promise(resolve => { - ipcMain.once('keydown', (event, key) => resolve(key)) - }) - ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' }) - expect(await keyDown).to.equal('b') - }) - - it('has the correct properties', async () => { - await w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - const testBeforeInput = async (opts) => { - const modifiers = [] - if (opts.shift) modifiers.push('shift') - if (opts.control) modifiers.push('control') - if (opts.alt) modifiers.push('alt') - if (opts.meta) modifiers.push('meta') - if (opts.isAutoRepeat) modifiers.push('isAutoRepeat') - - const p = emittedOnce(w.webContents, 'before-input-event') - w.webContents.sendInputEvent({ - type: opts.type, - keyCode: opts.keyCode, - modifiers: modifiers - }) - const [, input] = await p - - expect(input.type).to.equal(opts.type) - expect(input.key).to.equal(opts.key) - expect(input.code).to.equal(opts.code) - expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat) - expect(input.shift).to.equal(opts.shift) - expect(input.control).to.equal(opts.control) - expect(input.alt).to.equal(opts.alt) - expect(input.meta).to.equal(opts.meta) - } - await testBeforeInput({ - type: 'keyDown', - key: 'A', - code: 'KeyA', - keyCode: 'a', - shift: true, - control: true, - alt: true, - meta: true, - isAutoRepeat: true - }) - await testBeforeInput({ - type: 'keyUp', - key: '.', - code: 'Period', - keyCode: '.', - shift: false, - control: true, - alt: true, - meta: false, - isAutoRepeat: false - }) - await testBeforeInput({ - type: 'keyUp', - key: '!', - code: 'Digit1', - keyCode: '1', - shift: true, - control: false, - alt: false, - meta: true, - isAutoRepeat: false - }) - await testBeforeInput({ - type: 'keyUp', - key: 'Tab', - code: 'Tab', - keyCode: 'Tab', - shift: false, - control: true, - alt: false, - meta: false, - isAutoRepeat: true - }) - }) - }) - - describe('zoom-changed', () => { - beforeEach(function () { - // On Mac, zooming isn't done with the mouse wheel. - if (process.platform === 'darwin') { - return closeWindow(w).then(() => { - w = null - this.skip() - }) - } - }) - - it('is emitted with the correct zooming info', async () => { - w.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - await emittedOnce(w.webContents, 'did-finish-load') - - const testZoomChanged = async ({ zoomingIn }) => { - const promise = emittedOnce(w.webContents, 'zoom-changed') - - w.webContents.sendInputEvent({ - type: 'mousewheel', - x: 300, - y: 300, - deltaX: 0, - deltaY: zoomingIn ? 1 : -1, - wheelTicksX: 0, - wheelTicksY: zoomingIn ? 1 : -1, - phase: 'began', - modifiers: ['control', 'meta'] - }) - - const [, zoomDirection] = await promise - expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out') - } - - await testZoomChanged({ zoomingIn: true }) - await testZoomChanged({ zoomingIn: false }) - }) - }) - describe('devtools window', () => { let testFn = it if (process.platform === 'darwin' && isCi) { @@ -439,353 +98,6 @@ describe('webContents module', () => { }) }) - describe('sendInputEvent(event)', () => { - beforeEach(async () => { - await w.loadFile(path.join(fixtures, 'pages', 'key-events.html')) - }) - - it('can send keydown events', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('a') - expect(code).to.equal('KeyA') - expect(keyCode).to.equal(65) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) - }) - - it('can send keydown events with modifiers', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Z') - expect(code).to.equal('KeyZ') - expect(keyCode).to.equal(90) - expect(shiftKey).to.be.true() - expect(ctrlKey).to.be.true() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) - }) - - it('can send keydown events with special keys', (done) => { - ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Tab') - expect(code).to.equal('Tab') - expect(keyCode).to.equal(9) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.true() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] }) - }) - - it('can send char events', (done) => { - ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('a') - expect(code).to.equal('KeyA') - expect(keyCode).to.equal(65) - expect(shiftKey).to.be.false() - expect(ctrlKey).to.be.false() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' }) - w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' }) - }) - - it('can send char events with modifiers', (done) => { - ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => { - expect(key).to.equal('Z') - expect(code).to.equal('KeyZ') - expect(keyCode).to.equal(90) - expect(shiftKey).to.be.true() - expect(ctrlKey).to.be.true() - expect(altKey).to.be.false() - done() - }) - w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' }) - w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] }) - }) - }) - - it('supports inserting CSS', async () => { - w.loadURL('about:blank') - await w.webContents.insertCSS('body { background-repeat: round; }') - const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') - expect(result).to.equal('round') - }) - - it('supports removing inserted CSS', async () => { - w.loadURL('about:blank') - const key = await w.webContents.insertCSS('body { background-repeat: round; }') - await w.webContents.removeInsertedCSS(key) - const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")') - expect(result).to.equal('repeat') - }) - - it('supports inspecting an element in the devtools', (done) => { - w.loadURL('about:blank') - w.webContents.once('devtools-opened', () => { done() }) - w.webContents.inspectElement(10, 10) - }) - - describe('startDrag({file, icon})', () => { - it('throws errors for a missing file or a missing/empty icon', () => { - expect(() => { - w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') }) - }).to.throw(`Must specify either 'file' or 'files' option`) - - expect(() => { - w.webContents.startDrag({ file: __filename }) - }).to.throw(`Must specify 'icon' option`) - - if (process.platform === 'darwin') { - expect(() => { - w.webContents.startDrag({ file: __filename, icon: __filename }) - }).to.throw(`Must specify non-empty 'icon' option`) - } - }) - }) - - describe('focus()', () => { - describe('when the web contents is hidden', () => { - it('does not blur the focused window', (done) => { - ipcMain.once('answer', (event, parentFocused, childFocused) => { - expect(parentFocused).to.be.true() - expect(childFocused).to.be.false() - done() - }) - w.show() - w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html')) - }) - }) - }) - - describe('getOSProcessId()', () => { - it('returns a valid procress id', async () => { - expect(w.webContents.getOSProcessId()).to.equal(0) - - await w.loadURL('about:blank') - expect(w.webContents.getOSProcessId()).to.be.above(0) - }) - }) - - describe('zoom api', () => { - const zoomScheme = remote.getGlobal('zoomScheme') - const hostZoomMap = { - host1: 0.3, - host2: 0.7, - host3: 0.2 - } - - before((done) => { - const protocol = session.defaultSession.protocol - protocol.registerStringProtocol(zoomScheme, (request, callback) => { - const response = `` - callback({ data: response, mimeType: 'text/html' }) - }, (error) => done(error)) - }) - - after((done) => { - const protocol = session.defaultSession.protocol - protocol.unregisterProtocol(zoomScheme, (error) => done(error)) - }) - - // TODO(codebytere): remove in Electron v8.0.0 - it('can set the correct zoom level (functions)', async () => { - try { - await w.loadURL('about:blank') - const zoomLevel = w.webContents.getZoomLevel() - expect(zoomLevel).to.eql(0.0) - w.webContents.setZoomLevel(0.5) - const newZoomLevel = w.webContents.getZoomLevel() - expect(newZoomLevel).to.eql(0.5) - } finally { - w.webContents.setZoomLevel(0) - } - }) - - it('can set the correct zoom level', async () => { - try { - await w.loadURL('about:blank') - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.eql(0.0) - w.webContents.zoomLevel = 0.5 - const newZoomLevel = w.webContents.zoomLevel - expect(newZoomLevel).to.eql(0.5) - } finally { - w.webContents.zoomLevel = 0 - } - }) - - it('can persist zoom level across navigation', (done) => { - let finalNavigation = false - ipcMain.on('set-zoom', (e, host) => { - const zoomLevel = hostZoomMap[host] - if (!finalNavigation) w.webContents.zoomLevel = zoomLevel - console.log() - e.sender.send(`${host}-zoom-set`) - }) - ipcMain.on('host1-zoom-level', (e, zoomLevel) => { - const expectedZoomLevel = hostZoomMap.host1 - expect(zoomLevel).to.equal(expectedZoomLevel) - if (finalNavigation) { - done() - } else { - w.loadURL(`${zoomScheme}://host2`) - } - }) - ipcMain.once('host2-zoom-level', (e, zoomLevel) => { - const expectedZoomLevel = hostZoomMap.host2 - expect(zoomLevel).to.equal(expectedZoomLevel) - finalNavigation = true - w.webContents.goBack() - }) - w.loadURL(`${zoomScheme}://host1`) - }) - - it('can propagate zoom level across same session', (done) => { - const w2 = new BrowserWindow({ - show: false - }) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(hostZoomMap.host3) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel1).to.equal(zoomLevel2) - w2.setClosable(true) - w2.close() - done() - }) - w.webContents.on('did-finish-load', () => { - w.webContents.zoomLevel = hostZoomMap.host3 - w2.loadURL(`${zoomScheme}://host3`) - }) - w.loadURL(`${zoomScheme}://host3`) - }) - - it('cannot propagate zoom level across different session', (done) => { - const w2 = new BrowserWindow({ - show: false, - webPreferences: { - partition: 'temp' - } - }) - const protocol = w2.webContents.session.protocol - protocol.registerStringProtocol(zoomScheme, (request, callback) => { - callback('hello') - }, (error) => { - if (error) return done(error) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(hostZoomMap.host3) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel2).to.equal(0) - expect(zoomLevel1).to.not.equal(zoomLevel2) - - protocol.unregisterProtocol(zoomScheme, (error) => { - if (error) return done(error) - w2.setClosable(true) - w2.close() - done() - }) - }) - w.webContents.on('did-finish-load', () => { - w.webContents.zoomLevel = hostZoomMap.host3 - w2.loadURL(`${zoomScheme}://host3`) - }) - w.loadURL(`${zoomScheme}://host3`) - }) - }) - - it('can persist when it contains iframe', (done) => { - const server = http.createServer((req, res) => { - setTimeout(() => { - res.end() - }, 200) - }) - server.listen(0, '127.0.0.1', () => { - const url = 'http://127.0.0.1:' + server.address().port - const content = `` - w.webContents.on('did-frame-finish-load', (e, isMainFrame) => { - if (!isMainFrame) { - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.equal(2.0) - - w.webContents.zoomLevel = 0 - server.close() - done() - } - }) - w.webContents.on('dom-ready', () => { - w.webContents.zoomLevel = 2.0 - }) - w.loadURL(`data:text/html,${content}`) - }) - }) - - it('cannot propagate when used with webframe', (done) => { - let finalZoomLevel = 0 - const w2 = new BrowserWindow({ - show: false - }) - w2.webContents.on('did-finish-load', () => { - const zoomLevel1 = w.webContents.zoomLevel - expect(zoomLevel1).to.equal(finalZoomLevel) - - const zoomLevel2 = w2.webContents.zoomLevel - expect(zoomLevel2).to.equal(0) - expect(zoomLevel1).to.not.equal(zoomLevel2) - - w2.setClosable(true) - w2.close() - done() - }) - ipcMain.once('temporary-zoom-set', (e, zoomLevel) => { - w2.loadFile(path.join(fixtures, 'pages', 'c.html')) - finalZoomLevel = zoomLevel - }) - w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html')) - }) - - it('cannot persist zoom level after navigation with webFrame', (done) => { - let initialNavigation = true - const source = ` - const {ipcRenderer, webFrame} = require('electron') - webFrame.setZoomLevel(0.6) - ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel()) - ` - w.webContents.on('did-finish-load', () => { - if (initialNavigation) { - w.webContents.executeJavaScript(source) - } else { - const zoomLevel = w.webContents.zoomLevel - expect(zoomLevel).to.equal(0) - done() - } - }) - ipcMain.once('zoom-level-set', (e, zoomLevel) => { - expect(zoomLevel).to.equal(0.6) - w.loadFile(path.join(fixtures, 'pages', 'd.html')) - initialNavigation = false - }) - w.loadFile(path.join(fixtures, 'pages', 'c.html')) - }) - }) - describe('webrtc ip policy api', () => { it('can set and get webrtc ip policies', () => { const policies = [