api-web-contents-spec.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  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. }
  30. })
  31. })
  32. afterEach(() => closeWindow(w).then(() => { w = null }))
  33. describe('getAllWebContents() API', () => {
  34. it('returns an array of web contents', (done) => {
  35. w.webContents.on('devtools-opened', () => {
  36. const all = webContents.getAllWebContents().sort((a, b) => {
  37. return a.id - b.id
  38. })
  39. assert.ok(all.length >= 4)
  40. assert.strictEqual(all[0].getType(), 'window')
  41. assert.strictEqual(all[all.length - 2].getType(), 'webview')
  42. assert.strictEqual(all[all.length - 1].getType(), 'remote')
  43. done()
  44. })
  45. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-factor.html'))
  46. w.webContents.on('did-attach-webview', () => {
  47. w.webContents.openDevTools()
  48. })
  49. })
  50. })
  51. describe('getFocusedWebContents() API', () => {
  52. it('returns the focused web contents', (done) => {
  53. if (isCi) return done()
  54. const specWebContents = remote.getCurrentWebContents()
  55. assert.strictEqual(specWebContents.id, webContents.getFocusedWebContents().id)
  56. specWebContents.once('devtools-opened', () => {
  57. assert.strictEqual(specWebContents.devToolsWebContents.id, webContents.getFocusedWebContents().id)
  58. specWebContents.closeDevTools()
  59. })
  60. specWebContents.once('devtools-closed', () => {
  61. assert.strictEqual(specWebContents.id, webContents.getFocusedWebContents().id)
  62. done()
  63. })
  64. specWebContents.openDevTools()
  65. })
  66. it('does not crash when called on a detached dev tools window', (done) => {
  67. const specWebContents = w.webContents
  68. specWebContents.once('devtools-opened', () => {
  69. assert.doesNotThrow(() => {
  70. webContents.getFocusedWebContents()
  71. })
  72. specWebContents.closeDevTools()
  73. })
  74. specWebContents.once('devtools-closed', () => {
  75. assert.doesNotThrow(() => {
  76. webContents.getFocusedWebContents()
  77. })
  78. done()
  79. })
  80. specWebContents.openDevTools({ mode: 'detach' })
  81. w.inspectElement(100, 100)
  82. })
  83. })
  84. describe('setDevToolsWebContents() API', () => {
  85. it('sets arbitry webContents as devtools', (done) => {
  86. const devtools = new BrowserWindow({ show: false })
  87. devtools.webContents.once('dom-ready', () => {
  88. assert.ok(devtools.getURL().startsWith('chrome-devtools://devtools'))
  89. devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name', (name) => {
  90. assert.ok(name, 'InspectorFrontendHostImpl')
  91. devtools.destroy()
  92. done()
  93. })
  94. })
  95. w.webContents.setDevToolsWebContents(devtools.webContents)
  96. w.webContents.openDevTools()
  97. })
  98. })
  99. describe('isFocused() API', () => {
  100. it('returns false when the window is hidden', () => {
  101. BrowserWindow.getAllWindows().forEach((window) => {
  102. assert.strictEqual(!window.isVisible() && window.webContents.isFocused(), false)
  103. })
  104. })
  105. })
  106. describe('isCurrentlyAudible() API', () => {
  107. it('returns whether audio is playing', async () => {
  108. const webContents = remote.getCurrentWebContents()
  109. const context = new window.AudioContext()
  110. // Start in suspended state, because of the
  111. // new web audio api policy.
  112. context.suspend()
  113. const oscillator = context.createOscillator()
  114. oscillator.connect(context.destination)
  115. oscillator.start()
  116. let p = emittedOnce(webContents, '-audio-state-changed')
  117. await context.resume()
  118. const [, audible] = await p
  119. assert(webContents.isCurrentlyAudible() === audible)
  120. expect(webContents.isCurrentlyAudible()).to.be.true()
  121. p = emittedOnce(webContents, '-audio-state-changed')
  122. oscillator.stop()
  123. await p
  124. expect(webContents.isCurrentlyAudible()).to.be.false()
  125. oscillator.disconnect()
  126. context.close()
  127. })
  128. })
  129. describe('getWebPreferences() API', () => {
  130. it('should not crash when called for devTools webContents', (done) => {
  131. w.webContents.openDevTools()
  132. w.webContents.once('devtools-opened', () => {
  133. assert(!w.devToolsWebContents.getWebPreferences())
  134. done()
  135. })
  136. })
  137. })
  138. describe('before-input-event event', () => {
  139. it('can prevent document keyboard events', (done) => {
  140. ipcMain.once('keydown', (event, key) => {
  141. assert.strictEqual(key, 'b')
  142. done()
  143. })
  144. w.webContents.once('did-finish-load', () => {
  145. ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id)
  146. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' })
  147. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' })
  148. })
  149. w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  150. })
  151. it('has the correct properties', (done) => {
  152. w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
  153. w.webContents.once('did-finish-load', () => {
  154. const testBeforeInput = (opts) => {
  155. return new Promise((resolve, reject) => {
  156. w.webContents.once('before-input-event', (event, input) => {
  157. assert.strictEqual(input.type, opts.type)
  158. assert.strictEqual(input.key, opts.key)
  159. assert.strictEqual(input.code, opts.code)
  160. assert.strictEqual(input.isAutoRepeat, opts.isAutoRepeat)
  161. assert.strictEqual(input.shift, opts.shift)
  162. assert.strictEqual(input.control, opts.control)
  163. assert.strictEqual(input.alt, opts.alt)
  164. assert.strictEqual(input.meta, opts.meta)
  165. resolve()
  166. })
  167. const modifiers = []
  168. if (opts.shift) modifiers.push('shift')
  169. if (opts.control) modifiers.push('control')
  170. if (opts.alt) modifiers.push('alt')
  171. if (opts.meta) modifiers.push('meta')
  172. if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
  173. w.webContents.sendInputEvent({
  174. type: opts.type,
  175. keyCode: opts.keyCode,
  176. modifiers: modifiers
  177. })
  178. })
  179. }
  180. Promise.resolve().then(() => {
  181. return testBeforeInput({
  182. type: 'keyDown',
  183. key: 'A',
  184. code: 'KeyA',
  185. keyCode: 'a',
  186. shift: true,
  187. control: true,
  188. alt: true,
  189. meta: true,
  190. isAutoRepeat: true
  191. })
  192. }).then(() => {
  193. return testBeforeInput({
  194. type: 'keyUp',
  195. key: '.',
  196. code: 'Period',
  197. keyCode: '.',
  198. shift: false,
  199. control: true,
  200. alt: true,
  201. meta: false,
  202. isAutoRepeat: false
  203. })
  204. }).then(() => {
  205. return testBeforeInput({
  206. type: 'keyUp',
  207. key: '!',
  208. code: 'Digit1',
  209. keyCode: '1',
  210. shift: true,
  211. control: false,
  212. alt: false,
  213. meta: true,
  214. isAutoRepeat: false
  215. })
  216. }).then(() => {
  217. return testBeforeInput({
  218. type: 'keyUp',
  219. key: 'Tab',
  220. code: 'Tab',
  221. keyCode: 'Tab',
  222. shift: false,
  223. control: true,
  224. alt: false,
  225. meta: false,
  226. isAutoRepeat: true
  227. })
  228. }).then(done).catch(done)
  229. })
  230. })
  231. })
  232. describe('devtools window', () => {
  233. let testFn = it
  234. if (process.platform === 'darwin' && isCi) {
  235. testFn = it.skip
  236. }
  237. try {
  238. // We have other tests that check if native modules work, if we fail to require
  239. // robotjs let's skip this test to avoid false negatives
  240. require('robotjs')
  241. } catch (err) {
  242. testFn = it.skip
  243. }
  244. testFn('can receive and handle menu events', async function () {
  245. this.timeout(5000)
  246. w.show()
  247. w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  248. // Ensure the devtools are loaded
  249. w.webContents.closeDevTools()
  250. const opened = emittedOnce(w.webContents, 'devtools-opened')
  251. w.webContents.openDevTools()
  252. await opened
  253. await emittedOnce(w.webContents.devToolsWebContents, 'did-finish-load')
  254. w.webContents.devToolsWebContents.focus()
  255. // Focus an input field
  256. await w.webContents.devToolsWebContents.executeJavaScript(
  257. `const input = document.createElement('input');
  258. document.body.innerHTML = '';
  259. document.body.appendChild(input)
  260. input.focus();`
  261. )
  262. // Write something to the clipboard
  263. clipboard.writeText('test value')
  264. // Fake a paste request using robotjs to emulate a REAL keyboard paste event
  265. require('robotjs').keyTap('v', process.platform === 'darwin' ? ['command'] : ['control'])
  266. const start = Date.now()
  267. let val
  268. // Check every now and again for the pasted value (paste is async)
  269. while (val !== 'test value' && Date.now() - start <= 1000) {
  270. val = await w.webContents.devToolsWebContents.executeJavaScript(
  271. `document.querySelector('input').value`
  272. )
  273. await new Promise(resolve => setTimeout(resolve, 10))
  274. }
  275. // Once we're done expect the paste to have been successful
  276. expect(val).to.equal('test value', 'value should eventually become the pasted value')
  277. })
  278. })
  279. describe('sendInputEvent(event)', () => {
  280. beforeEach((done) => {
  281. w.loadFile(path.join(fixtures, 'pages', 'key-events.html'))
  282. w.webContents.once('did-finish-load', () => done())
  283. })
  284. it('can send keydown events', (done) => {
  285. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  286. assert.strictEqual(key, 'a')
  287. assert.strictEqual(code, 'KeyA')
  288. assert.strictEqual(keyCode, 65)
  289. assert.strictEqual(shiftKey, false)
  290. assert.strictEqual(ctrlKey, false)
  291. assert.strictEqual(altKey, false)
  292. done()
  293. })
  294. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
  295. })
  296. it('can send keydown events with modifiers', (done) => {
  297. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  298. assert.strictEqual(key, 'Z')
  299. assert.strictEqual(code, 'KeyZ')
  300. assert.strictEqual(keyCode, 90)
  301. assert.strictEqual(shiftKey, true)
  302. assert.strictEqual(ctrlKey, true)
  303. assert.strictEqual(altKey, false)
  304. done()
  305. })
  306. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
  307. })
  308. it('can send keydown events with special keys', (done) => {
  309. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  310. assert.strictEqual(key, 'Tab')
  311. assert.strictEqual(code, 'Tab')
  312. assert.strictEqual(keyCode, 9)
  313. assert.strictEqual(shiftKey, false)
  314. assert.strictEqual(ctrlKey, false)
  315. assert.strictEqual(altKey, true)
  316. done()
  317. })
  318. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] })
  319. })
  320. it('can send char events', (done) => {
  321. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  322. assert.strictEqual(key, 'a')
  323. assert.strictEqual(code, 'KeyA')
  324. assert.strictEqual(keyCode, 65)
  325. assert.strictEqual(shiftKey, false)
  326. assert.strictEqual(ctrlKey, false)
  327. assert.strictEqual(altKey, false)
  328. done()
  329. })
  330. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' })
  331. w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' })
  332. })
  333. it('can send char events with modifiers', (done) => {
  334. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  335. assert.strictEqual(key, 'Z')
  336. assert.strictEqual(code, 'KeyZ')
  337. assert.strictEqual(keyCode, 90)
  338. assert.strictEqual(shiftKey, true)
  339. assert.strictEqual(ctrlKey, true)
  340. assert.strictEqual(altKey, false)
  341. done()
  342. })
  343. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' })
  344. w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] })
  345. })
  346. })
  347. it('supports inserting CSS', (done) => {
  348. w.loadURL('about:blank')
  349. w.webContents.insertCSS('body { background-repeat: round; }')
  350. w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")', (result) => {
  351. assert.strictEqual(result, 'round')
  352. done()
  353. })
  354. })
  355. it('supports inspecting an element in the devtools', (done) => {
  356. w.loadURL('about:blank')
  357. w.webContents.once('devtools-opened', () => {
  358. done()
  359. })
  360. w.webContents.inspectElement(10, 10)
  361. })
  362. describe('startDrag({file, icon})', () => {
  363. it('throws errors for a missing file or a missing/empty icon', () => {
  364. assert.throws(() => {
  365. w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') })
  366. }, /Must specify either 'file' or 'files' option/)
  367. assert.throws(() => {
  368. w.webContents.startDrag({ file: __filename })
  369. }, /Must specify 'icon' option/)
  370. if (process.platform === 'darwin') {
  371. assert.throws(() => {
  372. w.webContents.startDrag({ file: __filename, icon: __filename })
  373. }, /Must specify non-empty 'icon' option/)
  374. }
  375. })
  376. })
  377. describe('focus()', () => {
  378. describe('when the web contents is hidden', () => {
  379. it('does not blur the focused window', (done) => {
  380. ipcMain.once('answer', (event, parentFocused, childFocused) => {
  381. assert.strictEqual(parentFocused, true)
  382. assert.strictEqual(childFocused, false)
  383. done()
  384. })
  385. w.show()
  386. w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html'))
  387. })
  388. })
  389. })
  390. describe('getOSProcessId()', () => {
  391. it('returns a valid procress id', (done) => {
  392. assert.strictEqual(w.webContents.getOSProcessId(), 0)
  393. w.webContents.once('did-finish-load', () => {
  394. const pid = w.webContents.getOSProcessId()
  395. assert.strictEqual(typeof pid, 'number')
  396. assert(pid > 0, `pid ${pid} is not greater than 0`)
  397. done()
  398. })
  399. w.loadURL('about:blank')
  400. })
  401. })
  402. describe('zoom api', () => {
  403. const zoomScheme = remote.getGlobal('zoomScheme')
  404. const hostZoomMap = {
  405. host1: 0.3,
  406. host2: 0.7,
  407. host3: 0.2
  408. }
  409. before((done) => {
  410. const protocol = session.defaultSession.protocol
  411. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  412. const response = `<script>
  413. const {ipcRenderer, remote} = require('electron')
  414. ipcRenderer.send('set-zoom', window.location.hostname)
  415. ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
  416. remote.getCurrentWebContents().getZoomLevel((zoomLevel) => {
  417. ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
  418. })
  419. })
  420. </script>`
  421. callback({ data: response, mimeType: 'text/html' })
  422. }, (error) => done(error))
  423. })
  424. after((done) => {
  425. const protocol = session.defaultSession.protocol
  426. protocol.unregisterProtocol(zoomScheme, (error) => done(error))
  427. })
  428. it('can set the correct zoom level', (done) => {
  429. w.webContents.on('did-finish-load', () => {
  430. w.webContents.getZoomLevel((zoomLevel) => {
  431. assert.strictEqual(zoomLevel, 0.0)
  432. w.webContents.setZoomLevel(0.5)
  433. w.webContents.getZoomLevel((zoomLevel) => {
  434. assert.strictEqual(zoomLevel, 0.5)
  435. w.webContents.setZoomLevel(0)
  436. done()
  437. })
  438. })
  439. })
  440. w.loadURL('about:blank')
  441. })
  442. it('can persist zoom level across navigation', (done) => {
  443. let finalNavigation = false
  444. ipcMain.on('set-zoom', (e, host) => {
  445. const zoomLevel = hostZoomMap[host]
  446. if (!finalNavigation) w.webContents.setZoomLevel(zoomLevel)
  447. e.sender.send(`${host}-zoom-set`)
  448. })
  449. ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
  450. const expectedZoomLevel = hostZoomMap.host1
  451. assert.strictEqual(zoomLevel, expectedZoomLevel)
  452. if (finalNavigation) {
  453. done()
  454. } else {
  455. w.loadURL(`${zoomScheme}://host2`)
  456. }
  457. })
  458. ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
  459. const expectedZoomLevel = hostZoomMap.host2
  460. assert.strictEqual(zoomLevel, expectedZoomLevel)
  461. finalNavigation = true
  462. w.webContents.goBack()
  463. })
  464. w.loadURL(`${zoomScheme}://host1`)
  465. })
  466. it('can propagate zoom level across same session', (done) => {
  467. const w2 = new BrowserWindow({
  468. show: false
  469. })
  470. w2.webContents.on('did-finish-load', () => {
  471. w.webContents.getZoomLevel((zoomLevel1) => {
  472. assert.strictEqual(zoomLevel1, hostZoomMap.host3)
  473. w2.webContents.getZoomLevel((zoomLevel2) => {
  474. assert.strictEqual(zoomLevel1, zoomLevel2)
  475. w2.setClosable(true)
  476. w2.close()
  477. done()
  478. })
  479. })
  480. })
  481. w.webContents.on('did-finish-load', () => {
  482. w.webContents.setZoomLevel(hostZoomMap.host3)
  483. w2.loadURL(`${zoomScheme}://host3`)
  484. })
  485. w.loadURL(`${zoomScheme}://host3`)
  486. })
  487. it('cannot propagate zoom level across different session', (done) => {
  488. const w2 = new BrowserWindow({
  489. show: false,
  490. webPreferences: {
  491. partition: 'temp'
  492. }
  493. })
  494. const protocol = w2.webContents.session.protocol
  495. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  496. callback('hello')
  497. }, (error) => {
  498. if (error) return done(error)
  499. w2.webContents.on('did-finish-load', () => {
  500. w.webContents.getZoomLevel((zoomLevel1) => {
  501. assert.strictEqual(zoomLevel1, hostZoomMap.host3)
  502. w2.webContents.getZoomLevel((zoomLevel2) => {
  503. assert.strictEqual(zoomLevel2, 0)
  504. assert.notStrictEqual(zoomLevel1, zoomLevel2)
  505. protocol.unregisterProtocol(zoomScheme, (error) => {
  506. if (error) return done(error)
  507. w2.setClosable(true)
  508. w2.close()
  509. done()
  510. })
  511. })
  512. })
  513. })
  514. w.webContents.on('did-finish-load', () => {
  515. w.webContents.setZoomLevel(hostZoomMap.host3)
  516. w2.loadURL(`${zoomScheme}://host3`)
  517. })
  518. w.loadURL(`${zoomScheme}://host3`)
  519. })
  520. })
  521. it('can persist when it contains iframe', (done) => {
  522. const server = http.createServer((req, res) => {
  523. setTimeout(() => {
  524. res.end()
  525. }, 200)
  526. })
  527. server.listen(0, '127.0.0.1', () => {
  528. const url = 'http://127.0.0.1:' + server.address().port
  529. const content = `<iframe src=${url}></iframe>`
  530. w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
  531. if (!isMainFrame) {
  532. w.webContents.getZoomLevel((zoomLevel) => {
  533. assert.strictEqual(zoomLevel, 2.0)
  534. w.webContents.setZoomLevel(0)
  535. server.close()
  536. done()
  537. })
  538. }
  539. })
  540. w.webContents.on('dom-ready', () => {
  541. w.webContents.setZoomLevel(2.0)
  542. })
  543. w.loadURL(`data:text/html,${content}`)
  544. })
  545. })
  546. it('cannot propagate when used with webframe', (done) => {
  547. let finalZoomLevel = 0
  548. const w2 = new BrowserWindow({
  549. show: false
  550. })
  551. w2.webContents.on('did-finish-load', () => {
  552. w.webContents.getZoomLevel((zoomLevel1) => {
  553. assert.strictEqual(zoomLevel1, finalZoomLevel)
  554. w2.webContents.getZoomLevel((zoomLevel2) => {
  555. assert.strictEqual(zoomLevel2, 0)
  556. assert.notStrictEqual(zoomLevel1, zoomLevel2)
  557. w2.setClosable(true)
  558. w2.close()
  559. done()
  560. })
  561. })
  562. })
  563. ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
  564. w2.loadFile(path.join(fixtures, 'pages', 'c.html'))
  565. finalZoomLevel = zoomLevel
  566. })
  567. w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html'))
  568. })
  569. it('cannot persist zoom level after navigation with webFrame', (done) => {
  570. let initialNavigation = true
  571. const source = `
  572. const {ipcRenderer, webFrame} = require('electron')
  573. webFrame.setZoomLevel(0.6)
  574. ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
  575. `
  576. w.webContents.on('did-finish-load', () => {
  577. if (initialNavigation) {
  578. w.webContents.executeJavaScript(source, () => {})
  579. } else {
  580. w.webContents.getZoomLevel((zoomLevel) => {
  581. assert.strictEqual(zoomLevel, 0)
  582. done()
  583. })
  584. }
  585. })
  586. ipcMain.once('zoom-level-set', (e, zoomLevel) => {
  587. assert.strictEqual(zoomLevel, 0.6)
  588. w.loadFile(path.join(fixtures, 'pages', 'd.html'))
  589. initialNavigation = false
  590. })
  591. w.loadFile(path.join(fixtures, 'pages', 'c.html'))
  592. })
  593. })
  594. describe('webrtc ip policy api', () => {
  595. it('can set and get webrtc ip policies', () => {
  596. const policies = [
  597. 'default',
  598. 'default_public_interface_only',
  599. 'default_public_and_private_interfaces',
  600. 'disable_non_proxied_udp'
  601. ]
  602. policies.forEach((policy) => {
  603. w.webContents.setWebRTCIPHandlingPolicy(policy)
  604. assert.strictEqual(w.webContents.getWebRTCIPHandlingPolicy(), policy)
  605. })
  606. })
  607. })
  608. describe('will-prevent-unload event', () => {
  609. it('does not emit if beforeunload returns undefined', (done) => {
  610. w.once('closed', () => {
  611. done()
  612. })
  613. w.webContents.on('will-prevent-unload', (e) => {
  614. assert.fail('should not have fired')
  615. })
  616. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-undefined.html'))
  617. })
  618. it('emits if beforeunload returns false', (done) => {
  619. w.webContents.on('will-prevent-unload', () => {
  620. done()
  621. })
  622. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  623. })
  624. it('supports calling preventDefault on will-prevent-unload events', (done) => {
  625. ipcRenderer.send('prevent-next-will-prevent-unload', w.webContents.id)
  626. w.once('closed', () => done())
  627. w.loadFile(path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  628. })
  629. })
  630. describe('setIgnoreMenuShortcuts(ignore)', () => {
  631. it('does not throw', () => {
  632. assert.strictEqual(w.webContents.setIgnoreMenuShortcuts(true), undefined)
  633. assert.strictEqual(w.webContents.setIgnoreMenuShortcuts(false), undefined)
  634. })
  635. })
  636. describe('create()', () => {
  637. it('does not crash on exit', async () => {
  638. const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontents.js')
  639. const electronPath = remote.getGlobal('process').execPath
  640. const appProcess = ChildProcess.spawn(electronPath, [appPath])
  641. const [code] = await emittedOnce(appProcess, 'close')
  642. expect(code).to.equal(0)
  643. })
  644. })
  645. // Destroying webContents in its event listener is going to crash when
  646. // Electron is built in Debug mode.
  647. xdescribe('destroy()', () => {
  648. let server
  649. before((done) => {
  650. server = http.createServer((request, response) => {
  651. switch (request.url) {
  652. case '/404':
  653. response.statusCode = '404'
  654. response.end()
  655. break
  656. case '/301':
  657. response.statusCode = '301'
  658. response.setHeader('Location', '/200')
  659. response.end()
  660. break
  661. case '/200':
  662. response.statusCode = '200'
  663. response.end('hello')
  664. break
  665. default:
  666. done('unsupported endpoint')
  667. }
  668. }).listen(0, '127.0.0.1', () => {
  669. server.url = 'http://127.0.0.1:' + server.address().port
  670. done()
  671. })
  672. })
  673. after(() => {
  674. server.close()
  675. server = null
  676. })
  677. it('should not crash when invoked synchronously inside navigation observer', (done) => {
  678. const events = [
  679. { name: 'did-start-loading', url: `${server.url}/200` },
  680. { name: 'dom-ready', url: `${server.url}/200` },
  681. { name: 'did-stop-loading', url: `${server.url}/200` },
  682. { name: 'did-finish-load', url: `${server.url}/200` },
  683. // FIXME: Multiple Emit calls inside an observer assume that object
  684. // will be alive till end of the observer. Synchronous `destroy` api
  685. // violates this contract and crashes.
  686. // { name: 'did-frame-finish-load', url: `${server.url}/200` },
  687. { name: 'did-fail-load', url: `${server.url}/404` }
  688. ]
  689. const responseEvent = 'webcontents-destroyed'
  690. function * genNavigationEvent () {
  691. let eventOptions = null
  692. while ((eventOptions = events.shift()) && events.length) {
  693. eventOptions.responseEvent = responseEvent
  694. ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
  695. yield 1
  696. }
  697. }
  698. const gen = genNavigationEvent()
  699. ipcRenderer.on(responseEvent, () => {
  700. if (!gen.next().value) done()
  701. })
  702. gen.next()
  703. })
  704. })
  705. describe('did-change-theme-color event', () => {
  706. it('is triggered with correct theme color', (done) => {
  707. let count = 0
  708. w.webContents.on('did-change-theme-color', (e, color) => {
  709. if (count === 0) {
  710. count += 1
  711. assert.strictEqual(color, '#FFEEDD')
  712. w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))
  713. } else if (count === 1) {
  714. assert.strictEqual(color, null)
  715. done()
  716. }
  717. })
  718. w.loadFile(path.join(fixtures, 'pages', 'theme-color.html'))
  719. })
  720. })
  721. describe('console-message event', () => {
  722. it('is triggered with correct log message', (done) => {
  723. w.webContents.on('console-message', (e, level, message) => {
  724. // Don't just assert as Chromium might emit other logs that we should ignore.
  725. if (message === 'a') {
  726. done()
  727. }
  728. })
  729. w.loadFile(path.join(fixtures, 'pages', 'a.html'))
  730. })
  731. })
  732. describe('referrer', () => {
  733. it('propagates referrer information to new target=_blank windows', (done) => {
  734. const server = http.createServer((req, res) => {
  735. if (req.url === '/should_have_referrer') {
  736. assert.strictEqual(req.headers.referer, 'http://127.0.0.1:' + server.address().port + '/')
  737. return done()
  738. }
  739. res.end('<a id="a" href="/should_have_referrer" target="_blank">link</a>')
  740. })
  741. server.listen(0, '127.0.0.1', () => {
  742. const url = 'http://127.0.0.1:' + server.address().port + '/'
  743. w.webContents.once('did-finish-load', () => {
  744. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  745. assert.strictEqual(referrer.url, url)
  746. assert.strictEqual(referrer.policy, 'no-referrer-when-downgrade')
  747. })
  748. w.webContents.executeJavaScript('a.click()')
  749. })
  750. w.loadURL(url)
  751. })
  752. })
  753. // TODO(jeremy): window.open() in a real browser passes the referrer, but
  754. // our hacked-up window.open() shim doesn't. It should.
  755. xit('propagates referrer information to windows opened with window.open', (done) => {
  756. const server = http.createServer((req, res) => {
  757. if (req.url === '/should_have_referrer') {
  758. assert.strictEqual(req.headers.referer, 'http://127.0.0.1:' + server.address().port + '/')
  759. return done()
  760. }
  761. res.end('')
  762. })
  763. server.listen(0, '127.0.0.1', () => {
  764. const url = 'http://127.0.0.1:' + server.address().port + '/'
  765. w.webContents.once('did-finish-load', () => {
  766. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  767. assert.strictEqual(referrer.url, url)
  768. assert.strictEqual(referrer.policy, 'no-referrer-when-downgrade')
  769. })
  770. w.webContents.executeJavaScript('window.open(location.href + "should_have_referrer")')
  771. })
  772. w.loadURL(url)
  773. })
  774. })
  775. })
  776. describe('webframe messages in sandboxed contents', () => {
  777. it('responds to executeJavaScript', (done) => {
  778. w.destroy()
  779. w = new BrowserWindow({
  780. show: false,
  781. webPreferences: {
  782. sandbox: true
  783. }
  784. })
  785. w.webContents.once('did-finish-load', () => {
  786. w.webContents.executeJavaScript('37 + 5', (result) => {
  787. assert.strictEqual(result, 42)
  788. done()
  789. })
  790. })
  791. w.loadURL('about:blank')
  792. })
  793. })
  794. describe('takeHeapSnapshot()', () => {
  795. it('works with sandboxed renderers', async () => {
  796. w.destroy()
  797. w = new BrowserWindow({
  798. show: false,
  799. webPreferences: {
  800. sandbox: true
  801. }
  802. })
  803. const p = emittedOnce(w.webContents, 'did-finish-load')
  804. w.loadURL('about:blank')
  805. await p
  806. const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot')
  807. const cleanup = () => {
  808. try {
  809. fs.unlinkSync(filePath)
  810. } catch (e) {
  811. // ignore error
  812. }
  813. }
  814. try {
  815. await w.webContents.takeHeapSnapshot(filePath)
  816. const stats = fs.statSync(filePath)
  817. expect(stats.size).not.to.be.equal(0)
  818. } finally {
  819. cleanup()
  820. }
  821. })
  822. it('fails with invalid file path', async () => {
  823. w.destroy()
  824. w = new BrowserWindow({
  825. show: false,
  826. webPreferences: {
  827. sandbox: true
  828. }
  829. })
  830. const p = emittedOnce(w.webContents, 'did-finish-load')
  831. w.loadURL('about:blank')
  832. await p
  833. const promise = w.webContents.takeHeapSnapshot('')
  834. return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed')
  835. })
  836. })
  837. describe('setBackgroundThrottling()', () => {
  838. it('does not crash when allowing', (done) => {
  839. w.webContents.setBackgroundThrottling(true)
  840. done()
  841. })
  842. it('does not crash when disallowing', (done) => {
  843. w.destroy()
  844. w = new BrowserWindow({
  845. show: false,
  846. width: 400,
  847. height: 400,
  848. webPreferences: {
  849. backgroundThrottling: true
  850. }
  851. })
  852. w.webContents.setBackgroundThrottling(false)
  853. done()
  854. })
  855. it('does not crash when called via BrowserWindow', (done) => {
  856. w.setBackgroundThrottling(true)
  857. done()
  858. })
  859. })
  860. describe('getPrinterList()', () => {
  861. before(function () {
  862. if (!features.isPrintingEnabled()) {
  863. return closeWindow(w).then(() => {
  864. w = null
  865. this.skip()
  866. })
  867. }
  868. })
  869. it('can get printer list', (done) => {
  870. w.destroy()
  871. w = new BrowserWindow({
  872. show: false,
  873. webPreferences: {
  874. sandbox: true
  875. }
  876. })
  877. w.webContents.once('did-finish-load', () => {
  878. const printers = w.webContents.getPrinters()
  879. assert.strictEqual(Array.isArray(printers), true)
  880. done()
  881. })
  882. w.loadURL('about:blank')
  883. })
  884. })
  885. describe('printToPDF()', () => {
  886. before(function () {
  887. if (!features.isPrintingEnabled()) {
  888. return closeWindow(w).then(() => {
  889. w = null
  890. this.skip()
  891. })
  892. }
  893. })
  894. it('can print to PDF', (done) => {
  895. w.destroy()
  896. w = new BrowserWindow({
  897. show: false,
  898. webPreferences: {
  899. sandbox: true
  900. }
  901. })
  902. w.webContents.once('did-finish-load', () => {
  903. w.webContents.printToPDF({}, function (error, data) {
  904. assert.strictEqual(error, null)
  905. assert.strictEqual(data instanceof Buffer, true)
  906. assert.notStrictEqual(data.length, 0)
  907. done()
  908. })
  909. })
  910. w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E')
  911. })
  912. })
  913. })