api-web-contents-spec.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  1. 'use strict'
  2. const assert = require('assert')
  3. const ChildProcess = require('child_process')
  4. const fs = require('fs')
  5. const http = require('http')
  6. const path = require('path')
  7. const { closeWindow } = require('./window-helpers')
  8. const { emittedOnce } = require('./events-helpers')
  9. const chai = require('chai')
  10. const dirtyChai = require('dirty-chai')
  11. const features = process.atomBinding('features')
  12. const { ipcRenderer, remote, clipboard } = require('electron')
  13. const { BrowserWindow, webContents, ipcMain, session } = remote
  14. const { expect } = chai
  15. const isCi = remote.getGlobal('isCi')
  16. chai.use(dirtyChai)
  17. /* The whole webContents API doesn't use standard callbacks */
  18. /* eslint-disable standard/no-callback-literal */
  19. describe('webContents module', () => {
  20. const fixtures = path.resolve(__dirname, 'fixtures')
  21. let w
  22. beforeEach(() => {
  23. w = new BrowserWindow({
  24. show: false,
  25. width: 400,
  26. height: 400,
  27. webPreferences: {
  28. backgroundThrottling: false,
  29. nodeIntegration: true,
  30. webviewTag: true
  31. }
  32. })
  33. })
  34. afterEach(() => closeWindow(w).then(() => { w = null }))
  35. describe('loadURL() promise API', () => {
  36. it('resolves when done loading', async () => {
  37. await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled
  38. })
  39. it('resolves when done loading a file URL', async () => {
  40. await expect(w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))).to.eventually.be.fulfilled
  41. })
  42. it('rejects when failing to load a file URL', async () => {
  43. await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected
  44. .and.have.property('code', 'ERR_FILE_NOT_FOUND')
  45. })
  46. it('rejects when loading fails due to DNS not resolved', async () => {
  47. await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected
  48. .and.have.property('code', 'ERR_NAME_NOT_RESOLVED')
  49. })
  50. it('rejects when navigation is cancelled due to a bad scheme', async () => {
  51. await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected
  52. .and.have.property('code', 'ERR_FAILED')
  53. })
  54. it('sets appropriate error information on rejection', async () => {
  55. let err
  56. try {
  57. await w.loadURL('file:non-existent')
  58. } catch (e) {
  59. err = e
  60. }
  61. expect(err).not.to.be.null()
  62. expect(err.code).to.eql('ERR_FILE_NOT_FOUND')
  63. expect(err.errno).to.eql(-6)
  64. expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent')
  65. })
  66. it('rejects if the load is aborted', async () => {
  67. const s = http.createServer((req, res) => { /* never complete the request */ })
  68. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
  69. const { port } = s.address()
  70. const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/)
  71. // load a different file before the first load completes, causing the
  72. // first load to be aborted.
  73. await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
  74. await p
  75. s.close()
  76. })
  77. it("doesn't reject when a subframe fails to load", async () => {
  78. let resp = null
  79. const s = http.createServer((req, res) => {
  80. res.writeHead(200, { 'Content-Type': 'text/html' })
  81. res.write('<iframe src="http://err.name.not.resolved"></iframe>')
  82. resp = res
  83. // don't end the response yet
  84. })
  85. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
  86. const { port } = s.address()
  87. const p = new Promise(resolve => {
  88. w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
  89. if (!isMainFrame) {
  90. resolve()
  91. }
  92. })
  93. })
  94. const main = w.loadURL(`http://127.0.0.1:${port}`)
  95. await p
  96. resp.end()
  97. await main
  98. s.close()
  99. })
  100. it("doesn't resolve when a subframe loads", async () => {
  101. let resp = null
  102. const s = http.createServer((req, res) => {
  103. res.writeHead(200, { 'Content-Type': 'text/html' })
  104. res.write('<iframe src="data:text/html,hi"></iframe>')
  105. resp = res
  106. // don't end the response yet
  107. })
  108. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve))
  109. const { port } = s.address()
  110. const p = new Promise(resolve => {
  111. w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
  112. if (!isMainFrame) {
  113. resolve()
  114. }
  115. })
  116. })
  117. const main = w.loadURL(`http://127.0.0.1:${port}`)
  118. await p
  119. resp.destroy() // cause the main request to fail
  120. await expect(main).to.eventually.be.rejected
  121. .and.have.property('errno', -355) // ERR_INCOMPLETE_CHUNKED_ENCODING
  122. s.close()
  123. })
  124. })
  125. describe('getAllWebContents() API', () => {
  126. it('returns an array of web contents', (done) => {
  127. w.webContents.on('devtools-opened', () => {
  128. const all = webContents.getAllWebContents().sort((a, b) => {
  129. return a.id - b.id
  130. })
  131. assert.ok(all.length >= 4)
  132. assert.strictEqual(all[0].getType(), 'window')
  133. assert.strictEqual(all[all.length - 2].getType(), 'webview')
  134. assert.strictEqual(all[all.length - 1].getType(), 'remote')
  135. done()
  136. })
  137. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-factor.html'))
  138. w.webContents.on('did-attach-webview', () => {
  139. w.webContents.openDevTools()
  140. })
  141. })
  142. })
  143. describe('getFocusedWebContents() API', () => {
  144. it('returns the focused web contents', (done) => {
  145. if (isCi) return done()
  146. const specWebContents = remote.getCurrentWebContents()
  147. assert.strictEqual(specWebContents.id, webContents.getFocusedWebContents().id)
  148. specWebContents.once('devtools-opened', () => {
  149. assert.strictEqual(specWebContents.devToolsWebContents.id, webContents.getFocusedWebContents().id)
  150. specWebContents.closeDevTools()
  151. })
  152. specWebContents.once('devtools-closed', () => {
  153. assert.strictEqual(specWebContents.id, webContents.getFocusedWebContents().id)
  154. done()
  155. })
  156. specWebContents.openDevTools()
  157. })
  158. it('does not crash when called on a detached dev tools window', (done) => {
  159. const specWebContents = w.webContents
  160. specWebContents.once('devtools-opened', () => {
  161. assert.doesNotThrow(() => {
  162. webContents.getFocusedWebContents()
  163. })
  164. specWebContents.closeDevTools()
  165. })
  166. specWebContents.once('devtools-closed', () => {
  167. assert.doesNotThrow(() => {
  168. webContents.getFocusedWebContents()
  169. })
  170. done()
  171. })
  172. specWebContents.openDevTools({ mode: 'detach' })
  173. w.inspectElement(100, 100)
  174. })
  175. })
  176. describe('setDevToolsWebContents() API', () => {
  177. it('sets arbitry webContents as devtools', (done) => {
  178. const devtools = new BrowserWindow({ show: false })
  179. devtools.webContents.once('dom-ready', () => {
  180. assert.ok(devtools.getURL().startsWith('chrome-devtools://devtools'))
  181. devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name', (name) => {
  182. assert.ok(name, 'InspectorFrontendHostImpl')
  183. devtools.destroy()
  184. done()
  185. })
  186. })
  187. w.webContents.setDevToolsWebContents(devtools.webContents)
  188. w.webContents.openDevTools()
  189. })
  190. })
  191. describe('isFocused() API', () => {
  192. it('returns false when the window is hidden', () => {
  193. BrowserWindow.getAllWindows().forEach((window) => {
  194. assert.strictEqual(!window.isVisible() && window.webContents.isFocused(), false)
  195. })
  196. })
  197. })
  198. describe('isCurrentlyAudible() API', () => {
  199. it('returns whether audio is playing', async () => {
  200. const webContents = remote.getCurrentWebContents()
  201. const context = new window.AudioContext()
  202. // Start in suspended state, because of the
  203. // new web audio api policy.
  204. context.suspend()
  205. const oscillator = context.createOscillator()
  206. oscillator.connect(context.destination)
  207. oscillator.start()
  208. let p = emittedOnce(webContents, '-audio-state-changed')
  209. await context.resume()
  210. const [, audible] = await p
  211. assert(webContents.isCurrentlyAudible() === audible)
  212. expect(webContents.isCurrentlyAudible()).to.be.true()
  213. p = emittedOnce(webContents, '-audio-state-changed')
  214. oscillator.stop()
  215. await p
  216. expect(webContents.isCurrentlyAudible()).to.be.false()
  217. oscillator.disconnect()
  218. context.close()
  219. })
  220. })
  221. describe('getWebPreferences() API', () => {
  222. it('should not crash when called for devTools webContents', (done) => {
  223. w.webContents.openDevTools()
  224. w.webContents.once('devtools-opened', () => {
  225. assert(!w.devToolsWebContents.getWebPreferences())
  226. done()
  227. })
  228. })
  229. })
  230. describe('openDevTools() API', () => {
  231. it('can show window with activation', async () => {
  232. const focused = emittedOnce(w, 'focus')
  233. w.show()
  234. await focused
  235. assert.strictEqual(w.isFocused(), true)
  236. const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened')
  237. w.webContents.openDevTools({ mode: 'detach', activate: true })
  238. await devtoolsOpened
  239. assert.strictEqual(w.isFocused(), false)
  240. })
  241. it('can show window without activation', async () => {
  242. const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened')
  243. w.webContents.openDevTools({ mode: 'detach', activate: false })
  244. await devtoolsOpened
  245. assert.strictEqual(w.isDevToolsOpened(), true)
  246. })
  247. })
  248. describe('before-input-event event', () => {
  249. it('can prevent document keyboard events', async () => {
  250. await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  251. const keyDown = new Promise(resolve => {
  252. ipcMain.once('keydown', (event, key) => resolve(key))
  253. })
  254. ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id)
  255. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' })
  256. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' })
  257. assert.strictEqual(await keyDown, 'b')
  258. })
  259. it('has the correct properties', async () => {
  260. await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
  261. const testBeforeInput = async (opts) => {
  262. const modifiers = []
  263. if (opts.shift) modifiers.push('shift')
  264. if (opts.control) modifiers.push('control')
  265. if (opts.alt) modifiers.push('alt')
  266. if (opts.meta) modifiers.push('meta')
  267. if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
  268. const p = emittedOnce(w.webContents, 'before-input-event')
  269. w.webContents.sendInputEvent({
  270. type: opts.type,
  271. keyCode: opts.keyCode,
  272. modifiers: modifiers
  273. })
  274. const [, input] = await p
  275. assert.strictEqual(input.type, opts.type)
  276. assert.strictEqual(input.key, opts.key)
  277. assert.strictEqual(input.code, opts.code)
  278. assert.strictEqual(input.isAutoRepeat, opts.isAutoRepeat)
  279. assert.strictEqual(input.shift, opts.shift)
  280. assert.strictEqual(input.control, opts.control)
  281. assert.strictEqual(input.alt, opts.alt)
  282. assert.strictEqual(input.meta, opts.meta)
  283. }
  284. await testBeforeInput({
  285. type: 'keyDown',
  286. key: 'A',
  287. code: 'KeyA',
  288. keyCode: 'a',
  289. shift: true,
  290. control: true,
  291. alt: true,
  292. meta: true,
  293. isAutoRepeat: true
  294. })
  295. await testBeforeInput({
  296. type: 'keyUp',
  297. key: '.',
  298. code: 'Period',
  299. keyCode: '.',
  300. shift: false,
  301. control: true,
  302. alt: true,
  303. meta: false,
  304. isAutoRepeat: false
  305. })
  306. await testBeforeInput({
  307. type: 'keyUp',
  308. key: '!',
  309. code: 'Digit1',
  310. keyCode: '1',
  311. shift: true,
  312. control: false,
  313. alt: false,
  314. meta: true,
  315. isAutoRepeat: false
  316. })
  317. await testBeforeInput({
  318. type: 'keyUp',
  319. key: 'Tab',
  320. code: 'Tab',
  321. keyCode: 'Tab',
  322. shift: false,
  323. control: true,
  324. alt: false,
  325. meta: false,
  326. isAutoRepeat: true
  327. })
  328. })
  329. })
  330. describe('devtools window', () => {
  331. let testFn = it
  332. if (process.platform === 'darwin' && isCi) {
  333. testFn = it.skip
  334. }
  335. try {
  336. // We have other tests that check if native modules work, if we fail to require
  337. // robotjs let's skip this test to avoid false negatives
  338. require('robotjs')
  339. } catch (err) {
  340. testFn = it.skip
  341. }
  342. testFn('can receive and handle menu events', async function () {
  343. this.timeout(5000)
  344. w.show()
  345. w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  346. // Ensure the devtools are loaded
  347. w.webContents.closeDevTools()
  348. const opened = emittedOnce(w.webContents, 'devtools-opened')
  349. w.webContents.openDevTools()
  350. await opened
  351. await emittedOnce(w.webContents.devToolsWebContents, 'did-finish-load')
  352. w.webContents.devToolsWebContents.focus()
  353. // Focus an input field
  354. await w.webContents.devToolsWebContents.executeJavaScript(
  355. `const input = document.createElement('input');
  356. document.body.innerHTML = '';
  357. document.body.appendChild(input)
  358. input.focus();`
  359. )
  360. // Write something to the clipboard
  361. clipboard.writeText('test value')
  362. // Fake a paste request using robotjs to emulate a REAL keyboard paste event
  363. require('robotjs').keyTap('v', process.platform === 'darwin' ? ['command'] : ['control'])
  364. const start = Date.now()
  365. let val
  366. // Check every now and again for the pasted value (paste is async)
  367. while (val !== 'test value' && Date.now() - start <= 1000) {
  368. val = await w.webContents.devToolsWebContents.executeJavaScript(
  369. `document.querySelector('input').value`
  370. )
  371. await new Promise(resolve => setTimeout(resolve, 10))
  372. }
  373. // Once we're done expect the paste to have been successful
  374. expect(val).to.equal('test value', 'value should eventually become the pasted value')
  375. })
  376. })
  377. describe('sendInputEvent(event)', () => {
  378. beforeEach(async () => {
  379. await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  380. })
  381. it('can send keydown events', (done) => {
  382. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  383. assert.strictEqual(key, 'a')
  384. assert.strictEqual(code, 'KeyA')
  385. assert.strictEqual(keyCode, 65)
  386. assert.strictEqual(shiftKey, false)
  387. assert.strictEqual(ctrlKey, false)
  388. assert.strictEqual(altKey, false)
  389. done()
  390. })
  391. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
  392. })
  393. it('can send keydown events with modifiers', (done) => {
  394. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  395. assert.strictEqual(key, 'Z')
  396. assert.strictEqual(code, 'KeyZ')
  397. assert.strictEqual(keyCode, 90)
  398. assert.strictEqual(shiftKey, true)
  399. assert.strictEqual(ctrlKey, true)
  400. assert.strictEqual(altKey, false)
  401. done()
  402. })
  403. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
  404. })
  405. it('can send keydown events with special keys', (done) => {
  406. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  407. assert.strictEqual(key, 'Tab')
  408. assert.strictEqual(code, 'Tab')
  409. assert.strictEqual(keyCode, 9)
  410. assert.strictEqual(shiftKey, false)
  411. assert.strictEqual(ctrlKey, false)
  412. assert.strictEqual(altKey, true)
  413. done()
  414. })
  415. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] })
  416. })
  417. it('can send char events', (done) => {
  418. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  419. assert.strictEqual(key, 'a')
  420. assert.strictEqual(code, 'KeyA')
  421. assert.strictEqual(keyCode, 65)
  422. assert.strictEqual(shiftKey, false)
  423. assert.strictEqual(ctrlKey, false)
  424. assert.strictEqual(altKey, false)
  425. done()
  426. })
  427. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
  428. w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' })
  429. })
  430. it('can send char events with modifiers', (done) => {
  431. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  432. assert.strictEqual(key, 'Z')
  433. assert.strictEqual(code, 'KeyZ')
  434. assert.strictEqual(keyCode, 90)
  435. assert.strictEqual(shiftKey, true)
  436. assert.strictEqual(ctrlKey, true)
  437. assert.strictEqual(altKey, false)
  438. done()
  439. })
  440. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' })
  441. w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
  442. })
  443. })
  444. it('supports inserting CSS', (done) => {
  445. w.loadURL('about:blank')
  446. w.webContents.insertCSS('body { background-repeat: round; }')
  447. w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")', (result) => {
  448. assert.strictEqual(result, 'round')
  449. done()
  450. })
  451. })
  452. it('supports inspecting an element in the devtools', (done) => {
  453. w.loadURL('about:blank')
  454. w.webContents.once('devtools-opened', () => {
  455. done()
  456. })
  457. w.webContents.inspectElement(10, 10)
  458. })
  459. describe('startDrag({file, icon})', () => {
  460. it('throws errors for a missing file or a missing/empty icon', () => {
  461. assert.throws(() => {
  462. w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') })
  463. }, /Must specify either 'file' or 'files' option/)
  464. assert.throws(() => {
  465. w.webContents.startDrag({ file: __filename })
  466. }, /Must specify 'icon' option/)
  467. if (process.platform === 'darwin') {
  468. assert.throws(() => {
  469. w.webContents.startDrag({ file: __filename, icon: __filename })
  470. }, /Must specify non-empty 'icon' option/)
  471. }
  472. })
  473. })
  474. describe('focus()', () => {
  475. describe('when the web contents is hidden', () => {
  476. it('does not blur the focused window', (done) => {
  477. ipcMain.once('answer', (event, parentFocused, childFocused) => {
  478. assert.strictEqual(parentFocused, true)
  479. assert.strictEqual(childFocused, false)
  480. done()
  481. })
  482. w.show()
  483. w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html'))
  484. })
  485. })
  486. })
  487. describe('getOSProcessId()', () => {
  488. it('returns a valid procress id', async () => {
  489. assert.strictEqual(w.webContents.getOSProcessId(), 0)
  490. await w.loadURL('about:blank')
  491. const pid = w.webContents.getOSProcessId()
  492. expect(pid).to.be.above(0)
  493. })
  494. })
  495. describe('zoom api', () => {
  496. const zoomScheme = remote.getGlobal('zoomScheme')
  497. const hostZoomMap = {
  498. host1: 0.3,
  499. host2: 0.7,
  500. host3: 0.2
  501. }
  502. before((done) => {
  503. const protocol = session.defaultSession.protocol
  504. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  505. const response = `<script>
  506. const {ipcRenderer, remote} = require('electron')
  507. ipcRenderer.send('set-zoom', window.location.hostname)
  508. ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
  509. const zoomLevel = remote.getCurrentWebContents().getZoomLevel()
  510. ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
  511. })
  512. </script>`
  513. callback({ data: response, mimeType: 'text/html' })
  514. }, (error) => done(error))
  515. })
  516. after((done) => {
  517. const protocol = session.defaultSession.protocol
  518. protocol.unregisterProtocol(zoomScheme, (error) => done(error))
  519. })
  520. it('can set the correct zoom level', async () => {
  521. try {
  522. await w.loadURL('about:blank')
  523. const zoomLevel = w.webContents.getZoomLevel()
  524. expect(zoomLevel).to.eql(0.0)
  525. w.webContents.setZoomLevel(0.5)
  526. const newZoomLevel = w.webContents.getZoomLevel()
  527. expect(newZoomLevel).to.eql(0.5)
  528. } finally {
  529. w.webContents.setZoomLevel(0)
  530. }
  531. })
  532. it('can set the correct zoom level (callback)', async () => {
  533. try {
  534. await w.loadURL('about:blank')
  535. const zoomLevel = await new Promise(resolve => w.webContents.getZoomLevel(resolve))
  536. expect(zoomLevel).to.eql(0.0)
  537. w.webContents.setZoomLevel(0.5)
  538. const newZoomLevel = await new Promise(resolve => w.webContents.getZoomLevel(resolve))
  539. expect(newZoomLevel).to.eql(0.5)
  540. } finally {
  541. w.webContents.setZoomLevel(0)
  542. }
  543. })
  544. it('can persist zoom level across navigation', (done) => {
  545. let finalNavigation = false
  546. ipcMain.on('set-zoom', (e, host) => {
  547. const zoomLevel = hostZoomMap[host]
  548. if (!finalNavigation) w.webContents.setZoomLevel(zoomLevel)
  549. e.sender.send(`${host}-zoom-set`)
  550. })
  551. ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
  552. const expectedZoomLevel = hostZoomMap.host1
  553. assert.strictEqual(zoomLevel, expectedZoomLevel)
  554. if (finalNavigation) {
  555. done()
  556. } else {
  557. w.loadURL(`${zoomScheme}://host2`)
  558. }
  559. })
  560. ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
  561. const expectedZoomLevel = hostZoomMap.host2
  562. assert.strictEqual(zoomLevel, expectedZoomLevel)
  563. finalNavigation = true
  564. w.webContents.goBack()
  565. })
  566. w.loadURL(`${zoomScheme}://host1`)
  567. })
  568. it('can propagate zoom level across same session', (done) => {
  569. const w2 = new BrowserWindow({
  570. show: false
  571. })
  572. w2.webContents.on('did-finish-load', () => {
  573. const zoomLevel1 = w.webContents.getZoomLevel()
  574. assert.strictEqual(zoomLevel1, hostZoomMap.host3)
  575. const zoomLevel2 = w2.webContents.getZoomLevel()
  576. assert.strictEqual(zoomLevel1, zoomLevel2)
  577. w2.setClosable(true)
  578. w2.close()
  579. done()
  580. })
  581. w.webContents.on('did-finish-load', () => {
  582. w.webContents.setZoomLevel(hostZoomMap.host3)
  583. w2.loadURL(`${zoomScheme}://host3`)
  584. })
  585. w.loadURL(`${zoomScheme}://host3`)
  586. })
  587. it('cannot propagate zoom level across different session', (done) => {
  588. const w2 = new BrowserWindow({
  589. show: false,
  590. webPreferences: {
  591. partition: 'temp'
  592. }
  593. })
  594. const protocol = w2.webContents.session.protocol
  595. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  596. callback('hello')
  597. }, (error) => {
  598. if (error) return done(error)
  599. w2.webContents.on('did-finish-load', () => {
  600. const zoomLevel1 = w.webContents.getZoomLevel()
  601. assert.strictEqual(zoomLevel1, hostZoomMap.host3)
  602. const zoomLevel2 = w2.webContents.getZoomLevel()
  603. assert.strictEqual(zoomLevel2, 0)
  604. assert.notStrictEqual(zoomLevel1, zoomLevel2)
  605. protocol.unregisterProtocol(zoomScheme, (error) => {
  606. if (error) return done(error)
  607. w2.setClosable(true)
  608. w2.close()
  609. done()
  610. })
  611. })
  612. w.webContents.on('did-finish-load', () => {
  613. w.webContents.setZoomLevel(hostZoomMap.host3)
  614. w2.loadURL(`${zoomScheme}://host3`)
  615. })
  616. w.loadURL(`${zoomScheme}://host3`)
  617. })
  618. })
  619. it('can persist when it contains iframe', (done) => {
  620. const server = http.createServer((req, res) => {
  621. setTimeout(() => {
  622. res.end()
  623. }, 200)
  624. })
  625. server.listen(0, '127.0.0.1', () => {
  626. const url = 'http://127.0.0.1:' + server.address().port
  627. const content = `<iframe src=${url}></iframe>`
  628. w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
  629. if (!isMainFrame) {
  630. const zoomLevel = w.webContents.getZoomLevel()
  631. assert.strictEqual(zoomLevel, 2.0)
  632. w.webContents.setZoomLevel(0)
  633. server.close()
  634. done()
  635. }
  636. })
  637. w.webContents.on('dom-ready', () => {
  638. w.webContents.setZoomLevel(2.0)
  639. })
  640. w.loadURL(`data:text/html,${content}`)
  641. })
  642. })
  643. it('cannot propagate when used with webframe', (done) => {
  644. let finalZoomLevel = 0
  645. const w2 = new BrowserWindow({
  646. show: false
  647. })
  648. w2.webContents.on('did-finish-load', () => {
  649. const zoomLevel1 = w.webContents.getZoomLevel()
  650. assert.strictEqual(zoomLevel1, finalZoomLevel)
  651. const zoomLevel2 = w2.webContents.getZoomLevel()
  652. assert.strictEqual(zoomLevel2, 0)
  653. assert.notStrictEqual(zoomLevel1, zoomLevel2)
  654. w2.setClosable(true)
  655. w2.close()
  656. done()
  657. })
  658. ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
  659. w2.loadFile(path.join(fixtures, 'pages', 'c.html'))
  660. finalZoomLevel = zoomLevel
  661. })
  662. w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html'))
  663. })
  664. it('cannot persist zoom level after navigation with webFrame', (done) => {
  665. let initialNavigation = true
  666. const source = `
  667. const {ipcRenderer, webFrame} = require('electron')
  668. webFrame.setZoomLevel(0.6)
  669. ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
  670. `
  671. w.webContents.on('did-finish-load', () => {
  672. if (initialNavigation) {
  673. w.webContents.executeJavaScript(source, () => {})
  674. } else {
  675. const zoomLevel = w.webContents.getZoomLevel()
  676. assert.strictEqual(zoomLevel, 0)
  677. done()
  678. }
  679. })
  680. ipcMain.once('zoom-level-set', (e, zoomLevel) => {
  681. assert.strictEqual(zoomLevel, 0.6)
  682. w.loadFile(path.join(fixtures, 'pages', 'd.html'))
  683. initialNavigation = false
  684. })
  685. w.loadFile(path.join(fixtures, 'pages', 'c.html'))
  686. })
  687. })
  688. describe('webrtc ip policy api', () => {
  689. it('can set and get webrtc ip policies', () => {
  690. const policies = [
  691. 'default',
  692. 'default_public_interface_only',
  693. 'default_public_and_private_interfaces',
  694. 'disable_non_proxied_udp'
  695. ]
  696. policies.forEach((policy) => {
  697. w.webContents.setWebRTCIPHandlingPolicy(policy)
  698. assert.strictEqual(w.webContents.getWebRTCIPHandlingPolicy(), policy)
  699. })
  700. })
  701. })
  702. describe('will-prevent-unload event', () => {
  703. it('does not emit if beforeunload returns undefined', (done) => {
  704. w.once('closed', () => {
  705. done()
  706. })
  707. w.webContents.on('will-prevent-unload', (e) => {
  708. assert.fail('should not have fired')
  709. })
  710. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-undefined.html'))
  711. })
  712. it('emits if beforeunload returns false', (done) => {
  713. w.webContents.on('will-prevent-unload', () => {
  714. done()
  715. })
  716. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  717. })
  718. it('supports calling preventDefault on will-prevent-unload events', (done) => {
  719. ipcRenderer.send('prevent-next-will-prevent-unload', w.webContents.id)
  720. w.once('closed', () => done())
  721. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  722. })
  723. })
  724. describe('render view deleted events', () => {
  725. let server = null
  726. before((done) => {
  727. server = http.createServer((req, res) => {
  728. const respond = () => {
  729. if (req.url === '/redirect-cross-site') {
  730. res.setHeader('Location', `${server.cross_site_url}/redirected`)
  731. res.statusCode = 302
  732. res.end()
  733. } else if (req.url === '/redirected') {
  734. res.end('<html><script>window.localStorage</script></html>')
  735. } else {
  736. res.end()
  737. }
  738. }
  739. setTimeout(respond, 0)
  740. })
  741. server.listen(0, '127.0.0.1', () => {
  742. server.url = `http://127.0.0.1:${server.address().port}`
  743. server.cross_site_url = `http://localhost:${server.address().port}`
  744. done()
  745. })
  746. })
  747. after(() => {
  748. server.close()
  749. server = null
  750. })
  751. it('does not emit current-render-view-deleted when speculative RVHs are deleted', (done) => {
  752. let currentRenderViewDeletedEmitted = false
  753. w.webContents.once('destroyed', () => {
  754. assert.strictEqual(currentRenderViewDeletedEmitted, false, 'current-render-view-deleted was emitted')
  755. done()
  756. })
  757. const renderViewDeletedHandler = () => {
  758. currentRenderViewDeletedEmitted = true
  759. }
  760. w.webContents.on('current-render-view-deleted', renderViewDeletedHandler)
  761. w.webContents.on('did-finish-load', (e) => {
  762. w.webContents.removeListener('current-render-view-deleted', renderViewDeletedHandler)
  763. w.close()
  764. })
  765. w.loadURL(`${server.url}/redirect-cross-site`)
  766. })
  767. it('emits current-render-view-deleted if the current RVHs are deleted', (done) => {
  768. let currentRenderViewDeletedEmitted = false
  769. w.webContents.once('destroyed', () => {
  770. assert.strictEqual(currentRenderViewDeletedEmitted, true, 'current-render-view-deleted wasn\'t emitted')
  771. done()
  772. })
  773. w.webContents.on('current-render-view-deleted', () => {
  774. currentRenderViewDeletedEmitted = true
  775. })
  776. w.webContents.on('did-finish-load', (e) => {
  777. w.close()
  778. })
  779. w.loadURL(`${server.url}/redirect-cross-site`)
  780. })
  781. it('emits render-view-deleted if any RVHs are deleted', (done) => {
  782. let rvhDeletedCount = 0
  783. w.webContents.once('destroyed', () => {
  784. const expectedRenderViewDeletedEventCount = 3 // 1 speculative upon redirection + 2 upon window close.
  785. assert.strictEqual(rvhDeletedCount, expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times')
  786. done()
  787. })
  788. w.webContents.on('render-view-deleted', () => {
  789. rvhDeletedCount++
  790. })
  791. w.webContents.on('did-finish-load', (e) => {
  792. w.close()
  793. })
  794. w.loadURL(`${server.url}/redirect-cross-site`)
  795. })
  796. })
  797. describe('setIgnoreMenuShortcuts(ignore)', () => {
  798. it('does not throw', () => {
  799. assert.strictEqual(w.webContents.setIgnoreMenuShortcuts(true), undefined)
  800. assert.strictEqual(w.webContents.setIgnoreMenuShortcuts(false), undefined)
  801. })
  802. })
  803. describe('create()', () => {
  804. it('does not crash on exit', async () => {
  805. const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontents.js')
  806. const electronPath = remote.getGlobal('process').execPath
  807. const appProcess = ChildProcess.spawn(electronPath, [appPath])
  808. const [code] = await emittedOnce(appProcess, 'close')
  809. expect(code).to.equal(0)
  810. })
  811. })
  812. // Destroying webContents in its event listener is going to crash when
  813. // Electron is built in Debug mode.
  814. xdescribe('destroy()', () => {
  815. let server
  816. before((done) => {
  817. server = http.createServer((request, response) => {
  818. switch (request.url) {
  819. case '/404':
  820. response.statusCode = '404'
  821. response.end()
  822. break
  823. case '/301':
  824. response.statusCode = '301'
  825. response.setHeader('Location', '/200')
  826. response.end()
  827. break
  828. case '/200':
  829. response.statusCode = '200'
  830. response.end('hello')
  831. break
  832. default:
  833. done('unsupported endpoint')
  834. }
  835. }).listen(0, '127.0.0.1', () => {
  836. server.url = 'http://127.0.0.1:' + server.address().port
  837. done()
  838. })
  839. })
  840. after(() => {
  841. server.close()
  842. server = null
  843. })
  844. it('should not crash when invoked synchronously inside navigation observer', (done) => {
  845. const events = [
  846. { name: 'did-start-loading', url: `${server.url}/200` },
  847. { name: 'dom-ready', url: `${server.url}/200` },
  848. { name: 'did-stop-loading', url: `${server.url}/200` },
  849. { name: 'did-finish-load', url: `${server.url}/200` },
  850. // FIXME: Multiple Emit calls inside an observer assume that object
  851. // will be alive till end of the observer. Synchronous `destroy` api
  852. // violates this contract and crashes.
  853. // { name: 'did-frame-finish-load', url: `${server.url}/200` },
  854. { name: 'did-fail-load', url: `${server.url}/404` }
  855. ]
  856. const responseEvent = 'webcontents-destroyed'
  857. function * genNavigationEvent () {
  858. let eventOptions = null
  859. while ((eventOptions = events.shift()) && events.length) {
  860. eventOptions.responseEvent = responseEvent
  861. ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
  862. yield 1
  863. }
  864. }
  865. const gen = genNavigationEvent()
  866. ipcRenderer.on(responseEvent, () => {
  867. if (!gen.next().value) done()
  868. })
  869. gen.next()
  870. })
  871. })
  872. describe('did-change-theme-color event', () => {
  873. it('is triggered with correct theme color', (done) => {
  874. let count = 0
  875. w.webContents.on('did-change-theme-color', (e, color) => {
  876. if (count === 0) {
  877. count += 1
  878. assert.strictEqual(color, '#FFEEDD')
  879. w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
  880. } else if (count === 1) {
  881. assert.strictEqual(color, null)
  882. done()
  883. }
  884. })
  885. w.loadFile(path.join(fixtures, 'pages', 'theme-color.html'))
  886. })
  887. })
  888. describe('console-message event', () => {
  889. it('is triggered with correct log message', (done) => {
  890. w.webContents.on('console-message', (e, level, message) => {
  891. // Don't just assert as Chromium might emit other logs that we should ignore.
  892. if (message === 'a') {
  893. done()
  894. }
  895. })
  896. w.loadFile(path.join(fixtures, 'pages', 'a.html'))
  897. })
  898. })
  899. describe('ipc-message event', () => {
  900. it('emits when the renderer process sends an asynchronous message', async () => {
  901. const webContents = remote.getCurrentWebContents()
  902. const promise = emittedOnce(webContents, 'ipc-message')
  903. ipcRenderer.send('message', 'Hello World!')
  904. const [, channel, message] = await promise
  905. expect(channel).to.equal('message')
  906. expect(message).to.equal('Hello World!')
  907. })
  908. })
  909. describe('ipc-message-sync event', () => {
  910. it('emits when the renderer process sends a synchronous message', async () => {
  911. const webContents = remote.getCurrentWebContents()
  912. const promise = emittedOnce(webContents, 'ipc-message-sync')
  913. ipcRenderer.send('handle-next-ipc-message-sync', 'foobar')
  914. const result = ipcRenderer.sendSync('message', 'Hello World!')
  915. const [, channel, message] = await promise
  916. expect(channel).to.equal('message')
  917. expect(message).to.equal('Hello World!')
  918. expect(result).to.equal('foobar')
  919. })
  920. })
  921. describe('referrer', () => {
  922. it('propagates referrer information to new target=_blank windows', (done) => {
  923. const server = http.createServer((req, res) => {
  924. if (req.url === '/should_have_referrer') {
  925. assert.strictEqual(req.headers.referer, 'http://127.0.0.1:' + server.address().port + '/')
  926. return done()
  927. }
  928. res.end('<a id="a" href="/should_have_referrer" target="_blank">link</a>')
  929. })
  930. server.listen(0, '127.0.0.1', () => {
  931. const url = 'http://127.0.0.1:' + server.address().port + '/'
  932. w.webContents.once('did-finish-load', () => {
  933. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  934. assert.strictEqual(referrer.url, url)
  935. assert.strictEqual(referrer.policy, 'no-referrer-when-downgrade')
  936. })
  937. w.webContents.executeJavaScript('a.click()')
  938. })
  939. w.loadURL(url)
  940. })
  941. })
  942. // TODO(jeremy): window.open() in a real browser passes the referrer, but
  943. // our hacked-up window.open() shim doesn't. It should.
  944. xit('propagates referrer information to windows opened with window.open', (done) => {
  945. const server = http.createServer((req, res) => {
  946. if (req.url === '/should_have_referrer') {
  947. assert.strictEqual(req.headers.referer, 'http://127.0.0.1:' + server.address().port + '/')
  948. return done()
  949. }
  950. res.end('')
  951. })
  952. server.listen(0, '127.0.0.1', () => {
  953. const url = 'http://127.0.0.1:' + server.address().port + '/'
  954. w.webContents.once('did-finish-load', () => {
  955. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  956. assert.strictEqual(referrer.url, url)
  957. assert.strictEqual(referrer.policy, 'no-referrer-when-downgrade')
  958. })
  959. w.webContents.executeJavaScript('window.open(location.href + "should_have_referrer")')
  960. })
  961. w.loadURL(url)
  962. })
  963. })
  964. })
  965. describe('webframe messages in sandboxed contents', () => {
  966. it('responds to executeJavaScript', (done) => {
  967. w.destroy()
  968. w = new BrowserWindow({
  969. show: false,
  970. webPreferences: {
  971. sandbox: true
  972. }
  973. })
  974. w.webContents.once('did-finish-load', () => {
  975. w.webContents.executeJavaScript('37 + 5', (result) => {
  976. assert.strictEqual(result, 42)
  977. done()
  978. })
  979. })
  980. w.loadURL('about:blank')
  981. })
  982. })
  983. describe('preload-error event', () => {
  984. const generateSpecs = (description, sandbox) => {
  985. describe(description, () => {
  986. it('is triggered when unhandled exception is thrown', async () => {
  987. const preload = path.join(fixtures, 'module', 'preload-error-exception.js')
  988. w.destroy()
  989. w = new BrowserWindow({
  990. show: false,
  991. webPreferences: {
  992. sandbox,
  993. preload
  994. }
  995. })
  996. const promise = emittedOnce(w.webContents, 'preload-error')
  997. w.loadURL('about:blank')
  998. const [, preloadPath, error] = await promise
  999. expect(preloadPath).to.equal(preload)
  1000. expect(error.message).to.equal('Hello World!')
  1001. })
  1002. it('is triggered on syntax errors', async () => {
  1003. const preload = path.join(fixtures, 'module', 'preload-error-syntax.js')
  1004. w.destroy()
  1005. w = new BrowserWindow({
  1006. show: false,
  1007. webPreferences: {
  1008. sandbox,
  1009. preload
  1010. }
  1011. })
  1012. const promise = emittedOnce(w.webContents, 'preload-error')
  1013. w.loadURL('about:blank')
  1014. const [, preloadPath, error] = await promise
  1015. expect(preloadPath).to.equal(preload)
  1016. expect(error.message).to.equal('foobar is not defined')
  1017. })
  1018. it('is triggered when preload script loading fails', async () => {
  1019. const preload = path.join(fixtures, 'module', 'preload-invalid.js')
  1020. w.destroy()
  1021. w = new BrowserWindow({
  1022. show: false,
  1023. webPreferences: {
  1024. sandbox,
  1025. preload
  1026. }
  1027. })
  1028. const promise = emittedOnce(w.webContents, 'preload-error')
  1029. w.loadURL('about:blank')
  1030. const [, preloadPath, error] = await promise
  1031. expect(preloadPath).to.equal(preload)
  1032. expect(error.message).to.contain('preload-invalid.js')
  1033. })
  1034. })
  1035. }
  1036. generateSpecs('without sandbox', false)
  1037. generateSpecs('with sandbox', true)
  1038. })
  1039. describe('takeHeapSnapshot()', () => {
  1040. it('works with sandboxed renderers', async () => {
  1041. w.destroy()
  1042. w = new BrowserWindow({
  1043. show: false,
  1044. webPreferences: {
  1045. sandbox: true
  1046. }
  1047. })
  1048. await w.loadURL('about:blank')
  1049. const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot')
  1050. const cleanup = () => {
  1051. try {
  1052. fs.unlinkSync(filePath)
  1053. } catch (e) {
  1054. // ignore error
  1055. }
  1056. }
  1057. try {
  1058. await w.webContents.takeHeapSnapshot(filePath)
  1059. const stats = fs.statSync(filePath)
  1060. expect(stats.size).not.to.be.equal(0)
  1061. } finally {
  1062. cleanup()
  1063. }
  1064. })
  1065. it('fails with invalid file path', async () => {
  1066. w.destroy()
  1067. w = new BrowserWindow({
  1068. show: false,
  1069. webPreferences: {
  1070. sandbox: true
  1071. }
  1072. })
  1073. await w.loadURL('about:blank')
  1074. const promise = w.webContents.takeHeapSnapshot('')
  1075. return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed')
  1076. })
  1077. })
  1078. describe('setBackgroundThrottling()', () => {
  1079. it('does not crash when allowing', (done) => {
  1080. w.webContents.setBackgroundThrottling(true)
  1081. done()
  1082. })
  1083. it('does not crash when disallowing', (done) => {
  1084. w.destroy()
  1085. w = new BrowserWindow({
  1086. show: false,
  1087. width: 400,
  1088. height: 400,
  1089. webPreferences: {
  1090. backgroundThrottling: true
  1091. }
  1092. })
  1093. w.webContents.setBackgroundThrottling(false)
  1094. done()
  1095. })
  1096. it('does not crash when called via BrowserWindow', (done) => {
  1097. w.setBackgroundThrottling(true)
  1098. done()
  1099. })
  1100. })
  1101. describe('getPrinterList()', () => {
  1102. before(function () {
  1103. if (!features.isPrintingEnabled()) {
  1104. return closeWindow(w).then(() => {
  1105. w = null
  1106. this.skip()
  1107. })
  1108. }
  1109. })
  1110. it('can get printer list', async () => {
  1111. w.destroy()
  1112. w = new BrowserWindow({
  1113. show: false,
  1114. webPreferences: {
  1115. sandbox: true
  1116. }
  1117. })
  1118. await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E')
  1119. const printers = w.webContents.getPrinters()
  1120. assert.strictEqual(Array.isArray(printers), true)
  1121. })
  1122. })
  1123. describe('printToPDF()', () => {
  1124. before(function () {
  1125. if (!features.isPrintingEnabled()) {
  1126. return closeWindow(w).then(() => {
  1127. w = null
  1128. this.skip()
  1129. })
  1130. }
  1131. })
  1132. it('can print to PDF', (done) => {
  1133. w.destroy()
  1134. w = new BrowserWindow({
  1135. show: false,
  1136. webPreferences: {
  1137. sandbox: true
  1138. }
  1139. })
  1140. w.webContents.once('did-finish-load', () => {
  1141. w.webContents.printToPDF({}, function (error, data) {
  1142. assert.strictEqual(error, null)
  1143. assert.strictEqual(data instanceof Buffer, true)
  1144. assert.notStrictEqual(data.length, 0)
  1145. done()
  1146. })
  1147. })
  1148. w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E')
  1149. })
  1150. it('does not crash when called multiple times', (done) => {
  1151. w.destroy()
  1152. w = new BrowserWindow({
  1153. show: false,
  1154. webPreferences: {
  1155. sandbox: true
  1156. }
  1157. })
  1158. w.webContents.once('did-finish-load', () => {
  1159. const count = 2
  1160. let current = 0
  1161. for (let i = 0; i < count; i++) {
  1162. w.webContents.printToPDF({}, function (error, data) {
  1163. assert.strictEqual(error, null)
  1164. assert.strictEqual(data instanceof Buffer, true)
  1165. assert.notStrictEqual(data.length, 0)
  1166. if (++current === count) {
  1167. done()
  1168. }
  1169. })
  1170. }
  1171. })
  1172. w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E')
  1173. })
  1174. })
  1175. })