api-web-contents-spec.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. 'use strict'
  2. const assert = require('assert')
  3. const http = require('http')
  4. const path = require('path')
  5. const {closeWindow} = require('./window-helpers')
  6. const {ipcRenderer, remote} = require('electron')
  7. const {BrowserWindow, webContents, ipcMain, session} = remote
  8. const isCi = remote.getGlobal('isCi')
  9. describe('webContents module', function () {
  10. const fixtures = path.resolve(__dirname, 'fixtures')
  11. let w
  12. beforeEach(function () {
  13. w = new BrowserWindow({
  14. show: false,
  15. width: 400,
  16. height: 400,
  17. webPreferences: {
  18. backgroundThrottling: false
  19. }
  20. })
  21. })
  22. afterEach(function () {
  23. return closeWindow(w).then(function () { w = null })
  24. })
  25. describe('getAllWebContents() API', function () {
  26. it('returns an array of web contents', function (done) {
  27. w.webContents.on('devtools-opened', function () {
  28. const all = webContents.getAllWebContents().sort(function (a, b) {
  29. return a.getId() - b.getId()
  30. })
  31. assert.ok(all.length >= 4)
  32. assert.equal(all[0].getType(), 'window')
  33. assert.equal(all[all.length - 2].getType(), 'remote')
  34. assert.equal(all[all.length - 1].getType(), 'webview')
  35. done()
  36. })
  37. w.loadURL('file://' + path.join(fixtures, 'pages', 'webview-zoom-factor.html'))
  38. w.webContents.openDevTools()
  39. })
  40. })
  41. describe('getFocusedWebContents() API', function () {
  42. it('returns the focused web contents', function (done) {
  43. if (isCi) return done()
  44. const specWebContents = remote.getCurrentWebContents()
  45. assert.equal(specWebContents.getId(), webContents.getFocusedWebContents().getId())
  46. specWebContents.once('devtools-opened', function () {
  47. assert.equal(specWebContents.devToolsWebContents.getId(), webContents.getFocusedWebContents().getId())
  48. specWebContents.closeDevTools()
  49. })
  50. specWebContents.once('devtools-closed', function () {
  51. assert.equal(specWebContents.getId(), webContents.getFocusedWebContents().getId())
  52. done()
  53. })
  54. specWebContents.openDevTools()
  55. })
  56. it('does not crash when called on a detached dev tools window', function (done) {
  57. const specWebContents = w.webContents
  58. specWebContents.once('devtools-opened', function () {
  59. assert.doesNotThrow(function () {
  60. webContents.getFocusedWebContents()
  61. })
  62. specWebContents.closeDevTools()
  63. })
  64. specWebContents.once('devtools-closed', function () {
  65. assert.doesNotThrow(function () {
  66. webContents.getFocusedWebContents()
  67. })
  68. done()
  69. })
  70. specWebContents.openDevTools({mode: 'detach'})
  71. w.inspectElement(100, 100)
  72. })
  73. })
  74. describe('isFocused() API', function () {
  75. it('returns false when the window is hidden', function () {
  76. BrowserWindow.getAllWindows().forEach(function (window) {
  77. assert.equal(!window.isVisible() && window.webContents.isFocused(), false)
  78. })
  79. })
  80. })
  81. describe('before-input-event event', () => {
  82. it('can prevent document keyboard events', (done) => {
  83. w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'key-events.html'))
  84. w.webContents.once('did-finish-load', () => {
  85. ipcMain.once('keydown', (event, key) => {
  86. assert.equal(key, 'b')
  87. done()
  88. })
  89. ipcRenderer.send('prevent-next-input-event', 'a', w.webContents.id)
  90. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'a'})
  91. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'b'})
  92. })
  93. })
  94. it('has the correct properties', (done) => {
  95. w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'base-page.html'))
  96. w.webContents.once('did-finish-load', () => {
  97. const testBeforeInput = (opts) => {
  98. return new Promise((resolve, reject) => {
  99. w.webContents.once('before-input-event', (event, input) => {
  100. assert.equal(input.type, opts.type)
  101. assert.equal(input.key, opts.key)
  102. assert.equal(input.code, opts.code)
  103. assert.equal(input.isAutoRepeat, opts.isAutoRepeat)
  104. assert.equal(input.shift, opts.shift)
  105. assert.equal(input.control, opts.control)
  106. assert.equal(input.alt, opts.alt)
  107. assert.equal(input.meta, opts.meta)
  108. resolve()
  109. })
  110. const modifiers = []
  111. if (opts.shift) modifiers.push('shift')
  112. if (opts.control) modifiers.push('control')
  113. if (opts.alt) modifiers.push('alt')
  114. if (opts.meta) modifiers.push('meta')
  115. if (opts.isAutoRepeat) modifiers.push('isAutoRepeat')
  116. w.webContents.sendInputEvent({
  117. type: opts.type,
  118. keyCode: opts.keyCode,
  119. modifiers: modifiers
  120. })
  121. })
  122. }
  123. Promise.resolve().then(() => {
  124. return testBeforeInput({
  125. type: 'keyDown',
  126. key: 'A',
  127. code: 'KeyA',
  128. keyCode: 'a',
  129. shift: true,
  130. control: true,
  131. alt: true,
  132. meta: true,
  133. isAutoRepeat: true
  134. })
  135. }).then(() => {
  136. return testBeforeInput({
  137. type: 'keyUp',
  138. key: '.',
  139. code: 'Period',
  140. keyCode: '.',
  141. shift: false,
  142. control: true,
  143. alt: true,
  144. meta: false,
  145. isAutoRepeat: false
  146. })
  147. }).then(() => {
  148. return testBeforeInput({
  149. type: 'keyUp',
  150. key: '!',
  151. code: 'Digit1',
  152. keyCode: '1',
  153. shift: true,
  154. control: false,
  155. alt: false,
  156. meta: true,
  157. isAutoRepeat: false
  158. })
  159. }).then(() => {
  160. return testBeforeInput({
  161. type: 'keyUp',
  162. key: 'Tab',
  163. code: 'Tab',
  164. keyCode: 'Tab',
  165. shift: false,
  166. control: true,
  167. alt: false,
  168. meta: false,
  169. isAutoRepeat: true
  170. })
  171. }).then(done).catch(done)
  172. })
  173. })
  174. })
  175. describe('sendInputEvent(event)', function () {
  176. beforeEach(function (done) {
  177. w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'key-events.html'))
  178. w.webContents.once('did-finish-load', function () {
  179. done()
  180. })
  181. })
  182. it('can send keydown events', function (done) {
  183. ipcMain.once('keydown', function (event, key, code, keyCode, shiftKey, ctrlKey, altKey) {
  184. assert.equal(key, 'a')
  185. assert.equal(code, 'KeyA')
  186. assert.equal(keyCode, 65)
  187. assert.equal(shiftKey, false)
  188. assert.equal(ctrlKey, false)
  189. assert.equal(altKey, false)
  190. done()
  191. })
  192. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'A'})
  193. })
  194. it('can send keydown events with modifiers', function (done) {
  195. ipcMain.once('keydown', function (event, key, code, keyCode, shiftKey, ctrlKey, altKey) {
  196. assert.equal(key, 'Z')
  197. assert.equal(code, 'KeyZ')
  198. assert.equal(keyCode, 90)
  199. assert.equal(shiftKey, true)
  200. assert.equal(ctrlKey, true)
  201. assert.equal(altKey, false)
  202. done()
  203. })
  204. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl']})
  205. })
  206. it('can send keydown events with special keys', function (done) {
  207. ipcMain.once('keydown', function (event, key, code, keyCode, shiftKey, ctrlKey, altKey) {
  208. assert.equal(key, 'Tab')
  209. assert.equal(code, 'Tab')
  210. assert.equal(keyCode, 9)
  211. assert.equal(shiftKey, false)
  212. assert.equal(ctrlKey, false)
  213. assert.equal(altKey, true)
  214. done()
  215. })
  216. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'Tab', modifiers: ['alt']})
  217. })
  218. it('can send char events', function (done) {
  219. ipcMain.once('keypress', function (event, key, code, keyCode, shiftKey, ctrlKey, altKey) {
  220. assert.equal(key, 'a')
  221. assert.equal(code, 'KeyA')
  222. assert.equal(keyCode, 65)
  223. assert.equal(shiftKey, false)
  224. assert.equal(ctrlKey, false)
  225. assert.equal(altKey, false)
  226. done()
  227. })
  228. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'A'})
  229. w.webContents.sendInputEvent({type: 'char', keyCode: 'A'})
  230. })
  231. it('can send char events with modifiers', function (done) {
  232. ipcMain.once('keypress', function (event, key, code, keyCode, shiftKey, ctrlKey, altKey) {
  233. assert.equal(key, 'Z')
  234. assert.equal(code, 'KeyZ')
  235. assert.equal(keyCode, 90)
  236. assert.equal(shiftKey, true)
  237. assert.equal(ctrlKey, true)
  238. assert.equal(altKey, false)
  239. done()
  240. })
  241. w.webContents.sendInputEvent({type: 'keyDown', keyCode: 'Z'})
  242. w.webContents.sendInputEvent({type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl']})
  243. })
  244. })
  245. it('supports inserting CSS', function (done) {
  246. w.loadURL('about:blank')
  247. w.webContents.insertCSS('body { background-repeat: round; }')
  248. w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")', (result) => {
  249. assert.equal(result, 'round')
  250. done()
  251. })
  252. })
  253. it('supports inspecting an element in the devtools', function (done) {
  254. w.loadURL('about:blank')
  255. w.webContents.once('devtools-opened', function () {
  256. done()
  257. })
  258. w.webContents.inspectElement(10, 10)
  259. })
  260. describe('startDrag({file, icon})', () => {
  261. it('throws errors for a missing file or a missing/empty icon', () => {
  262. assert.throws(() => {
  263. w.webContents.startDrag({icon: path.join(__dirname, 'fixtures', 'assets', 'logo.png')})
  264. }, /Must specify either 'file' or 'files' option/)
  265. assert.throws(() => {
  266. w.webContents.startDrag({file: __filename})
  267. }, /Must specify 'icon' option/)
  268. if (process.platform === 'darwin') {
  269. assert.throws(() => {
  270. w.webContents.startDrag({file: __filename, icon: __filename})
  271. }, /Must specify non-empty 'icon' option/)
  272. }
  273. })
  274. })
  275. describe('focus()', function () {
  276. describe('when the web contents is hidden', function () {
  277. it('does not blur the focused window', function (done) {
  278. ipcMain.once('answer', (event, parentFocused, childFocused) => {
  279. assert.equal(parentFocused, true)
  280. assert.equal(childFocused, false)
  281. done()
  282. })
  283. w.show()
  284. w.loadURL('file://' + path.join(__dirname, 'fixtures', 'pages', 'focus-web-contents.html'))
  285. })
  286. })
  287. })
  288. describe('zoom api', () => {
  289. const zoomScheme = remote.getGlobal('zoomScheme')
  290. const hostZoomMap = {
  291. host1: 0.3,
  292. host2: 0.7,
  293. host3: 0.2
  294. }
  295. before((done) => {
  296. const protocol = session.defaultSession.protocol
  297. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  298. const response = `<script>
  299. const {ipcRenderer, remote} = require('electron')
  300. ipcRenderer.send('set-zoom', window.location.hostname)
  301. ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
  302. remote.getCurrentWebContents().getZoomLevel((zoomLevel) => {
  303. ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
  304. })
  305. })
  306. </script>`
  307. callback({data: response, mimeType: 'text/html'})
  308. }, (error) => done(error))
  309. })
  310. after((done) => {
  311. const protocol = session.defaultSession.protocol
  312. protocol.unregisterProtocol(zoomScheme, (error) => done(error))
  313. })
  314. it('can set the correct zoom level', (done) => {
  315. w.loadURL('about:blank')
  316. w.webContents.on('did-finish-load', () => {
  317. w.webContents.getZoomLevel((zoomLevel) => {
  318. assert.equal(zoomLevel, 0.0)
  319. w.webContents.setZoomLevel(0.5)
  320. w.webContents.getZoomLevel((zoomLevel) => {
  321. assert.equal(zoomLevel, 0.5)
  322. w.webContents.setZoomLevel(0)
  323. done()
  324. })
  325. })
  326. })
  327. })
  328. it('can persist zoom level across navigation', (done) => {
  329. let finalNavigation = false
  330. ipcMain.on('set-zoom', (e, host) => {
  331. const zoomLevel = hostZoomMap[host]
  332. if (!finalNavigation) {
  333. w.webContents.setZoomLevel(zoomLevel)
  334. }
  335. e.sender.send(`${host}-zoom-set`)
  336. })
  337. ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
  338. const expectedZoomLevel = hostZoomMap.host1
  339. assert.equal(zoomLevel, expectedZoomLevel)
  340. if (finalNavigation) {
  341. done()
  342. } else {
  343. w.loadURL(`${zoomScheme}://host2`)
  344. }
  345. })
  346. ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
  347. const expectedZoomLevel = hostZoomMap.host2
  348. assert.equal(zoomLevel, expectedZoomLevel)
  349. finalNavigation = true
  350. w.webContents.goBack()
  351. })
  352. w.loadURL(`${zoomScheme}://host1`)
  353. })
  354. it('can propagate zoom level across same session', (done) => {
  355. const w2 = new BrowserWindow({
  356. show: false
  357. })
  358. w2.webContents.on('did-finish-load', () => {
  359. w.webContents.getZoomLevel((zoomLevel1) => {
  360. assert.equal(zoomLevel1, hostZoomMap.host3)
  361. w2.webContents.getZoomLevel((zoomLevel2) => {
  362. assert.equal(zoomLevel1, zoomLevel2)
  363. w2.setClosable(true)
  364. w2.close()
  365. done()
  366. })
  367. })
  368. })
  369. w.webContents.on('did-finish-load', () => {
  370. w.webContents.setZoomLevel(hostZoomMap.host3)
  371. w2.loadURL(`${zoomScheme}://host3`)
  372. })
  373. w.loadURL(`${zoomScheme}://host3`)
  374. })
  375. it('cannot propagate zoom level across different session', (done) => {
  376. const w2 = new BrowserWindow({
  377. show: false,
  378. webPreferences: {
  379. partition: 'temp'
  380. }
  381. })
  382. const protocol = w2.webContents.session.protocol
  383. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  384. callback('hello')
  385. }, (error) => {
  386. if (error) return done(error)
  387. w2.webContents.on('did-finish-load', () => {
  388. w.webContents.getZoomLevel((zoomLevel1) => {
  389. assert.equal(zoomLevel1, hostZoomMap.host3)
  390. w2.webContents.getZoomLevel((zoomLevel2) => {
  391. assert.equal(zoomLevel2, 0)
  392. assert.notEqual(zoomLevel1, zoomLevel2)
  393. protocol.unregisterProtocol(zoomScheme, (error) => {
  394. if (error) return done(error)
  395. w2.setClosable(true)
  396. w2.close()
  397. done()
  398. })
  399. })
  400. })
  401. })
  402. w.webContents.on('did-finish-load', () => {
  403. w.webContents.setZoomLevel(hostZoomMap.host3)
  404. w2.loadURL(`${zoomScheme}://host3`)
  405. })
  406. w.loadURL(`${zoomScheme}://host3`)
  407. })
  408. })
  409. it('can persist when it contains iframe', (done) => {
  410. const server = http.createServer(function (req, res) {
  411. setTimeout(() => {
  412. res.end()
  413. }, 200)
  414. })
  415. server.listen(0, '127.0.0.1', function () {
  416. const url = 'http://127.0.0.1:' + server.address().port
  417. const content = `<iframe src=${url}></iframe>`
  418. w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
  419. if (!isMainFrame) {
  420. w.webContents.getZoomLevel((zoomLevel) => {
  421. assert.equal(zoomLevel, 2.0)
  422. w.webContents.setZoomLevel(0)
  423. server.close()
  424. done()
  425. })
  426. }
  427. })
  428. w.webContents.on('dom-ready', () => {
  429. w.webContents.setZoomLevel(2.0)
  430. })
  431. w.loadURL(`data:text/html,${content}`)
  432. })
  433. })
  434. it('cannot propagate when used with webframe', (done) => {
  435. let finalZoomLevel = 0
  436. const w2 = new BrowserWindow({
  437. show: false
  438. })
  439. w2.webContents.on('did-finish-load', () => {
  440. w.webContents.getZoomLevel((zoomLevel1) => {
  441. assert.equal(zoomLevel1, finalZoomLevel)
  442. w2.webContents.getZoomLevel((zoomLevel2) => {
  443. assert.equal(zoomLevel2, 0)
  444. assert.notEqual(zoomLevel1, zoomLevel2)
  445. w2.setClosable(true)
  446. w2.close()
  447. done()
  448. })
  449. })
  450. })
  451. ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
  452. w2.loadURL(`file://${fixtures}/pages/c.html`)
  453. finalZoomLevel = zoomLevel
  454. })
  455. w.loadURL(`file://${fixtures}/pages/webframe-zoom.html`)
  456. })
  457. it('cannot persist zoom level after navigation with webFrame', (done) => {
  458. let initialNavigation = true
  459. const source = `
  460. const {ipcRenderer, webFrame} = require('electron')
  461. webFrame.setZoomLevel(0.6)
  462. ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
  463. `
  464. w.webContents.on('did-finish-load', () => {
  465. if (initialNavigation) {
  466. w.webContents.executeJavaScript(source, () => {})
  467. } else {
  468. w.webContents.getZoomLevel((zoomLevel) => {
  469. assert.equal(zoomLevel, 0)
  470. done()
  471. })
  472. }
  473. })
  474. ipcMain.once('zoom-level-set', (e, zoomLevel) => {
  475. assert.equal(zoomLevel, 0.6)
  476. w.loadURL(`file://${fixtures}/pages/d.html`)
  477. initialNavigation = false
  478. })
  479. w.loadURL(`file://${fixtures}/pages/c.html`)
  480. })
  481. })
  482. describe('webrtc ip policy api', () => {
  483. it('can set and get webrtc ip policies', () => {
  484. const policies = [
  485. 'default',
  486. 'default_public_interface_only',
  487. 'default_public_and_private_interfaces',
  488. 'disable_non_proxied_udp'
  489. ]
  490. policies.forEach((policy) => {
  491. w.webContents.setWebRTCIPHandlingPolicy(policy)
  492. assert.equal(w.webContents.getWebRTCIPHandlingPolicy(), policy)
  493. })
  494. })
  495. })
  496. describe('destroy()', () => {
  497. let server
  498. before(function (done) {
  499. server = http.createServer((request, response) => {
  500. switch (request.url) {
  501. case '/404':
  502. response.statusCode = '404'
  503. response.end()
  504. break
  505. case '/301':
  506. response.statusCode = '301'
  507. response.setHeader('Location', '/200')
  508. response.end()
  509. break
  510. case '/200':
  511. response.statusCode = '200'
  512. response.end('hello')
  513. break
  514. default:
  515. done('unsupported endpoint')
  516. }
  517. }).listen(0, '127.0.0.1', () => {
  518. server.url = 'http://127.0.0.1:' + server.address().port
  519. done()
  520. })
  521. })
  522. after(function () {
  523. server.close()
  524. server = null
  525. })
  526. it('should not crash when invoked synchronously inside navigation observer', (done) => {
  527. const events = [
  528. { name: 'did-start-loading', url: `${server.url}/200` },
  529. { name: 'did-get-redirect-request', url: `${server.url}/301` },
  530. { name: 'did-get-response-details', url: `${server.url}/200` },
  531. { name: 'dom-ready', url: `${server.url}/200` },
  532. { name: 'did-stop-loading', url: `${server.url}/200` },
  533. { name: 'did-finish-load', url: `${server.url}/200` },
  534. // FIXME: Multiple Emit calls inside an observer assume that object
  535. // will be alive till end of the observer. Synchronous `destroy` api
  536. // violates this contract and crashes.
  537. // { name: 'did-frame-finish-load', url: `${server.url}/200` },
  538. { name: 'did-fail-load', url: `${server.url}/404` }
  539. ]
  540. const responseEvent = 'webcontents-destroyed'
  541. function* genNavigationEvent () {
  542. let eventOptions = null
  543. while ((eventOptions = events.shift()) && events.length) {
  544. eventOptions.responseEvent = responseEvent
  545. ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
  546. yield 1
  547. }
  548. }
  549. let gen = genNavigationEvent()
  550. ipcRenderer.on(responseEvent, () => {
  551. if (!gen.next().value) done()
  552. })
  553. gen.next()
  554. })
  555. })
  556. })