api-web-contents-spec.js 43 KB

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