api-web-contents-spec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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('getOSProcessId()', function () {
  289. it('returns a valid procress id', function (done) {
  290. assert.strictEqual(w.webContents.getOSProcessId(), 0)
  291. w.webContents.once('did-finish-load', () => {
  292. const pid = w.webContents.getOSProcessId()
  293. assert.equal(typeof pid, 'number')
  294. assert(pid > 0, `pid ${pid} is not greater than 0`)
  295. done()
  296. })
  297. w.loadURL('about:blank')
  298. })
  299. })
  300. describe('zoom api', () => {
  301. const zoomScheme = remote.getGlobal('zoomScheme')
  302. const hostZoomMap = {
  303. host1: 0.3,
  304. host2: 0.7,
  305. host3: 0.2
  306. }
  307. before((done) => {
  308. const protocol = session.defaultSession.protocol
  309. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  310. const response = `<script>
  311. const {ipcRenderer, remote} = require('electron')
  312. ipcRenderer.send('set-zoom', window.location.hostname)
  313. ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
  314. remote.getCurrentWebContents().getZoomLevel((zoomLevel) => {
  315. ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
  316. })
  317. })
  318. </script>`
  319. callback({data: response, mimeType: 'text/html'})
  320. }, (error) => done(error))
  321. })
  322. after((done) => {
  323. const protocol = session.defaultSession.protocol
  324. protocol.unregisterProtocol(zoomScheme, (error) => done(error))
  325. })
  326. it('can set the correct zoom level', (done) => {
  327. w.loadURL('about:blank')
  328. w.webContents.on('did-finish-load', () => {
  329. w.webContents.getZoomLevel((zoomLevel) => {
  330. assert.equal(zoomLevel, 0.0)
  331. w.webContents.setZoomLevel(0.5)
  332. w.webContents.getZoomLevel((zoomLevel) => {
  333. assert.equal(zoomLevel, 0.5)
  334. w.webContents.setZoomLevel(0)
  335. done()
  336. })
  337. })
  338. })
  339. })
  340. it('can persist zoom level across navigation', (done) => {
  341. let finalNavigation = false
  342. ipcMain.on('set-zoom', (e, host) => {
  343. const zoomLevel = hostZoomMap[host]
  344. if (!finalNavigation) {
  345. w.webContents.setZoomLevel(zoomLevel)
  346. }
  347. e.sender.send(`${host}-zoom-set`)
  348. })
  349. ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
  350. const expectedZoomLevel = hostZoomMap.host1
  351. assert.equal(zoomLevel, expectedZoomLevel)
  352. if (finalNavigation) {
  353. done()
  354. } else {
  355. w.loadURL(`${zoomScheme}://host2`)
  356. }
  357. })
  358. ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
  359. const expectedZoomLevel = hostZoomMap.host2
  360. assert.equal(zoomLevel, expectedZoomLevel)
  361. finalNavigation = true
  362. w.webContents.goBack()
  363. })
  364. w.loadURL(`${zoomScheme}://host1`)
  365. })
  366. it('can propagate zoom level across same session', (done) => {
  367. const w2 = new BrowserWindow({
  368. show: false
  369. })
  370. w2.webContents.on('did-finish-load', () => {
  371. w.webContents.getZoomLevel((zoomLevel1) => {
  372. assert.equal(zoomLevel1, hostZoomMap.host3)
  373. w2.webContents.getZoomLevel((zoomLevel2) => {
  374. assert.equal(zoomLevel1, zoomLevel2)
  375. w2.setClosable(true)
  376. w2.close()
  377. done()
  378. })
  379. })
  380. })
  381. w.webContents.on('did-finish-load', () => {
  382. w.webContents.setZoomLevel(hostZoomMap.host3)
  383. w2.loadURL(`${zoomScheme}://host3`)
  384. })
  385. w.loadURL(`${zoomScheme}://host3`)
  386. })
  387. it('cannot propagate zoom level across different session', (done) => {
  388. const w2 = new BrowserWindow({
  389. show: false,
  390. webPreferences: {
  391. partition: 'temp'
  392. }
  393. })
  394. const protocol = w2.webContents.session.protocol
  395. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  396. callback('hello')
  397. }, (error) => {
  398. if (error) return done(error)
  399. w2.webContents.on('did-finish-load', () => {
  400. w.webContents.getZoomLevel((zoomLevel1) => {
  401. assert.equal(zoomLevel1, hostZoomMap.host3)
  402. w2.webContents.getZoomLevel((zoomLevel2) => {
  403. assert.equal(zoomLevel2, 0)
  404. assert.notEqual(zoomLevel1, zoomLevel2)
  405. protocol.unregisterProtocol(zoomScheme, (error) => {
  406. if (error) return done(error)
  407. w2.setClosable(true)
  408. w2.close()
  409. done()
  410. })
  411. })
  412. })
  413. })
  414. w.webContents.on('did-finish-load', () => {
  415. w.webContents.setZoomLevel(hostZoomMap.host3)
  416. w2.loadURL(`${zoomScheme}://host3`)
  417. })
  418. w.loadURL(`${zoomScheme}://host3`)
  419. })
  420. })
  421. it('can persist when it contains iframe', (done) => {
  422. const server = http.createServer(function (req, res) {
  423. setTimeout(() => {
  424. res.end()
  425. }, 200)
  426. })
  427. server.listen(0, '127.0.0.1', function () {
  428. const url = 'http://127.0.0.1:' + server.address().port
  429. const content = `<iframe src=${url}></iframe>`
  430. w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
  431. if (!isMainFrame) {
  432. w.webContents.getZoomLevel((zoomLevel) => {
  433. assert.equal(zoomLevel, 2.0)
  434. w.webContents.setZoomLevel(0)
  435. server.close()
  436. done()
  437. })
  438. }
  439. })
  440. w.webContents.on('dom-ready', () => {
  441. w.webContents.setZoomLevel(2.0)
  442. })
  443. w.loadURL(`data:text/html,${content}`)
  444. })
  445. })
  446. it('cannot propagate when used with webframe', (done) => {
  447. let finalZoomLevel = 0
  448. const w2 = new BrowserWindow({
  449. show: false
  450. })
  451. w2.webContents.on('did-finish-load', () => {
  452. w.webContents.getZoomLevel((zoomLevel1) => {
  453. assert.equal(zoomLevel1, finalZoomLevel)
  454. w2.webContents.getZoomLevel((zoomLevel2) => {
  455. assert.equal(zoomLevel2, 0)
  456. assert.notEqual(zoomLevel1, zoomLevel2)
  457. w2.setClosable(true)
  458. w2.close()
  459. done()
  460. })
  461. })
  462. })
  463. ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
  464. w2.loadURL(`file://${fixtures}/pages/c.html`)
  465. finalZoomLevel = zoomLevel
  466. })
  467. w.loadURL(`file://${fixtures}/pages/webframe-zoom.html`)
  468. })
  469. it('cannot persist zoom level after navigation with webFrame', (done) => {
  470. let initialNavigation = true
  471. const source = `
  472. const {ipcRenderer, webFrame} = require('electron')
  473. webFrame.setZoomLevel(0.6)
  474. ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
  475. `
  476. w.webContents.on('did-finish-load', () => {
  477. if (initialNavigation) {
  478. w.webContents.executeJavaScript(source, () => {})
  479. } else {
  480. w.webContents.getZoomLevel((zoomLevel) => {
  481. assert.equal(zoomLevel, 0)
  482. done()
  483. })
  484. }
  485. })
  486. ipcMain.once('zoom-level-set', (e, zoomLevel) => {
  487. assert.equal(zoomLevel, 0.6)
  488. w.loadURL(`file://${fixtures}/pages/d.html`)
  489. initialNavigation = false
  490. })
  491. w.loadURL(`file://${fixtures}/pages/c.html`)
  492. })
  493. })
  494. describe('webrtc ip policy api', () => {
  495. it('can set and get webrtc ip policies', () => {
  496. const policies = [
  497. 'default',
  498. 'default_public_interface_only',
  499. 'default_public_and_private_interfaces',
  500. 'disable_non_proxied_udp'
  501. ]
  502. policies.forEach((policy) => {
  503. w.webContents.setWebRTCIPHandlingPolicy(policy)
  504. assert.equal(w.webContents.getWebRTCIPHandlingPolicy(), policy)
  505. })
  506. })
  507. })
  508. describe('will-prevent-unload event', function () {
  509. it('does not emit if beforeunload returns undefined', function (done) {
  510. w.once('closed', function () {
  511. done()
  512. })
  513. w.webContents.on('will-prevent-unload', function (e) {
  514. assert.fail('should not have fired')
  515. })
  516. w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-undefined.html'))
  517. })
  518. it('emits if beforeunload returns false', (done) => {
  519. w.webContents.on('will-prevent-unload', () => {
  520. done()
  521. })
  522. w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  523. })
  524. it('supports calling preventDefault on will-prevent-unload events', function (done) {
  525. ipcRenderer.send('prevent-next-will-prevent-unload', w.webContents.id)
  526. w.once('closed', () => done())
  527. w.loadURL('file://' + path.join(fixtures, 'api', 'close-beforeunload-false.html'))
  528. })
  529. })
  530. describe('setIgnoreMenuShortcuts(ignore)', function () {
  531. it('does not throw', function () {
  532. assert.equal(w.webContents.setIgnoreMenuShortcuts(true), undefined)
  533. assert.equal(w.webContents.setIgnoreMenuShortcuts(false), undefined)
  534. })
  535. })
  536. describe('destroy()', () => {
  537. let server
  538. before(function (done) {
  539. server = http.createServer((request, response) => {
  540. switch (request.url) {
  541. case '/404':
  542. response.statusCode = '404'
  543. response.end()
  544. break
  545. case '/301':
  546. response.statusCode = '301'
  547. response.setHeader('Location', '/200')
  548. response.end()
  549. break
  550. case '/200':
  551. response.statusCode = '200'
  552. response.end('hello')
  553. break
  554. default:
  555. done('unsupported endpoint')
  556. }
  557. }).listen(0, '127.0.0.1', () => {
  558. server.url = 'http://127.0.0.1:' + server.address().port
  559. done()
  560. })
  561. })
  562. after(function () {
  563. server.close()
  564. server = null
  565. })
  566. it('should not crash when invoked synchronously inside navigation observer', (done) => {
  567. const events = [
  568. { name: 'did-start-loading', url: `${server.url}/200` },
  569. { name: 'did-get-redirect-request', url: `${server.url}/301` },
  570. { name: 'did-get-response-details', url: `${server.url}/200` },
  571. { name: 'dom-ready', url: `${server.url}/200` },
  572. { name: 'did-stop-loading', url: `${server.url}/200` },
  573. { name: 'did-finish-load', url: `${server.url}/200` },
  574. // FIXME: Multiple Emit calls inside an observer assume that object
  575. // will be alive till end of the observer. Synchronous `destroy` api
  576. // violates this contract and crashes.
  577. // { name: 'did-frame-finish-load', url: `${server.url}/200` },
  578. { name: 'did-fail-load', url: `${server.url}/404` }
  579. ]
  580. const responseEvent = 'webcontents-destroyed'
  581. function* genNavigationEvent () {
  582. let eventOptions = null
  583. while ((eventOptions = events.shift()) && events.length) {
  584. eventOptions.responseEvent = responseEvent
  585. ipcRenderer.send('test-webcontents-navigation-observer', eventOptions)
  586. yield 1
  587. }
  588. }
  589. let gen = genNavigationEvent()
  590. ipcRenderer.on(responseEvent, () => {
  591. if (!gen.next().value) done()
  592. })
  593. gen.next()
  594. })
  595. })
  596. })