webview-spec.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. const { expect } = require('chai')
  2. const path = require('path')
  3. const http = require('http')
  4. const url = require('url')
  5. const { ipcRenderer } = require('electron')
  6. const { emittedOnce, waitForEvent } = require('./events-helpers')
  7. const features = process.electronBinding('features')
  8. const nativeModulesEnabled = process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS
  9. /* Most of the APIs here don't use standard callbacks */
  10. /* eslint-disable standard/no-callback-literal */
  11. describe('<webview> tag', function () {
  12. this.timeout(3 * 60 * 1000)
  13. const fixtures = path.join(__dirname, 'fixtures')
  14. let webview = null
  15. const loadWebView = async (webview, attributes = {}) => {
  16. for (const [name, value] of Object.entries(attributes)) {
  17. webview.setAttribute(name, value)
  18. }
  19. document.body.appendChild(webview)
  20. await waitForEvent(webview, 'did-finish-load')
  21. return webview
  22. }
  23. const startLoadingWebViewAndWaitForMessage = async (webview, attributes = {}) => {
  24. loadWebView(webview, attributes) // Don't wait for load to be finished.
  25. const event = await waitForEvent(webview, 'console-message')
  26. return event.message
  27. }
  28. beforeEach(() => {
  29. webview = new WebView()
  30. })
  31. afterEach(() => {
  32. if (!document.body.contains(webview)) {
  33. document.body.appendChild(webview)
  34. }
  35. webview.remove()
  36. })
  37. describe('src attribute', () => {
  38. it('specifies the page to load', async () => {
  39. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  40. src: `file://${fixtures}/pages/a.html`
  41. })
  42. expect(message).to.equal('a')
  43. })
  44. it('navigates to new page when changed', async () => {
  45. await loadWebView(webview, {
  46. src: `file://${fixtures}/pages/a.html`
  47. })
  48. webview.src = `file://${fixtures}/pages/b.html`
  49. const { message } = await waitForEvent(webview, 'console-message')
  50. expect(message).to.equal('b')
  51. })
  52. it('resolves relative URLs', async () => {
  53. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  54. src: '../fixtures/pages/e.html'
  55. })
  56. expect(message).to.equal('Window script is loaded before preload script')
  57. })
  58. it('ignores empty values', () => {
  59. expect(webview.src).to.equal('')
  60. for (const emptyValue of ['', null, undefined]) {
  61. webview.src = emptyValue
  62. expect(webview.src).to.equal('')
  63. }
  64. })
  65. it('does not wait until loadURL is resolved', async () => {
  66. await loadWebView(webview, { src: 'about:blank' })
  67. const before = Date.now()
  68. webview.src = 'https://github.com'
  69. const now = Date.now()
  70. // Setting src is essentially sending a sync IPC message, which should
  71. // not exceed more than a few ms.
  72. //
  73. // This is for testing #18638.
  74. expect(now - before).to.be.below(100)
  75. })
  76. })
  77. describe('nodeintegration attribute', () => {
  78. it('inserts no node symbols when not set', async () => {
  79. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  80. src: `file://${fixtures}/pages/c.html`
  81. })
  82. const types = JSON.parse(message)
  83. expect(types).to.include({
  84. require: 'undefined',
  85. module: 'undefined',
  86. process: 'undefined',
  87. global: 'undefined'
  88. })
  89. })
  90. it('inserts node symbols when set', async () => {
  91. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  92. nodeintegration: 'on',
  93. src: `file://${fixtures}/pages/d.html`
  94. })
  95. const types = JSON.parse(message)
  96. expect(types).to.include({
  97. require: 'function',
  98. module: 'object',
  99. process: 'object'
  100. })
  101. })
  102. it('loads node symbols after POST navigation when set', async function () {
  103. // FIXME Figure out why this is timing out on AppVeyor
  104. if (process.env.APPVEYOR === 'True') {
  105. this.skip()
  106. return
  107. }
  108. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  109. nodeintegration: 'on',
  110. src: `file://${fixtures}/pages/post.html`
  111. })
  112. const types = JSON.parse(message)
  113. expect(types).to.include({
  114. require: 'function',
  115. module: 'object',
  116. process: 'object'
  117. })
  118. })
  119. it('disables node integration on child windows when it is disabled on the webview', async () => {
  120. const src = url.format({
  121. pathname: `${fixtures}/pages/webview-opener-no-node-integration.html`,
  122. protocol: 'file',
  123. query: {
  124. p: `${fixtures}/pages/window-opener-node.html`
  125. },
  126. slashes: true
  127. })
  128. loadWebView(webview, {
  129. allowpopups: 'on',
  130. src
  131. })
  132. const { message } = await waitForEvent(webview, 'console-message')
  133. expect(JSON.parse(message).isProcessGlobalUndefined).to.be.true()
  134. });
  135. (nativeModulesEnabled ? it : it.skip)('loads native modules when navigation happens', async function () {
  136. await loadWebView(webview, {
  137. nodeintegration: 'on',
  138. src: `file://${fixtures}/pages/native-module.html`
  139. })
  140. webview.reload()
  141. const { message } = await waitForEvent(webview, 'console-message')
  142. expect(message).to.equal('function')
  143. })
  144. })
  145. describe('enableremotemodule attribute', () => {
  146. const generateSpecs = (description, sandbox) => {
  147. describe(description, () => {
  148. const preload = `${fixtures}/module/preload-disable-remote.js`
  149. const src = `file://${fixtures}/api/blank.html`
  150. it('enables the remote module by default', async () => {
  151. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  152. preload,
  153. src,
  154. sandbox
  155. })
  156. const typeOfRemote = JSON.parse(message)
  157. expect(typeOfRemote).to.equal('object')
  158. })
  159. it('disables the remote module when false', async () => {
  160. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  161. preload,
  162. src,
  163. sandbox,
  164. enableremotemodule: false
  165. })
  166. const typeOfRemote = JSON.parse(message)
  167. expect(typeOfRemote).to.equal('undefined')
  168. })
  169. })
  170. }
  171. generateSpecs('without sandbox', false)
  172. generateSpecs('with sandbox', true)
  173. })
  174. describe('preload attribute', () => {
  175. it('loads the script before other scripts in window', async () => {
  176. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  177. preload: `${fixtures}/module/preload.js`,
  178. src: `file://${fixtures}/pages/e.html`
  179. })
  180. expect(message).to.be.a('string')
  181. expect(message).to.be.not.equal('Window script is loaded before preload script')
  182. })
  183. it('preload script can still use "process" and "Buffer" when nodeintegration is off', async () => {
  184. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  185. preload: `${fixtures}/module/preload-node-off.js`,
  186. src: `file://${fixtures}/api/blank.html`
  187. })
  188. const types = JSON.parse(message)
  189. expect(types).to.include({
  190. process: 'object',
  191. Buffer: 'function'
  192. })
  193. })
  194. it('runs in the correct scope when sandboxed', async () => {
  195. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  196. preload: `${fixtures}/module/preload-context.js`,
  197. src: `file://${fixtures}/api/blank.html`,
  198. webpreferences: 'sandbox=yes'
  199. })
  200. const types = JSON.parse(message)
  201. expect(types).to.include({
  202. require: 'function', // arguments passed to it should be availale
  203. electron: 'undefined', // objects from the scope it is called from should not be available
  204. window: 'object', // the window object should be available
  205. localVar: 'undefined' // but local variables should not be exposed to the window
  206. })
  207. })
  208. it('preload script can require modules that still use "process" and "Buffer" when nodeintegration is off', async () => {
  209. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  210. preload: `${fixtures}/module/preload-node-off-wrapper.js`,
  211. src: `file://${fixtures}/api/blank.html`
  212. })
  213. const types = JSON.parse(message)
  214. expect(types).to.include({
  215. process: 'object',
  216. Buffer: 'function'
  217. })
  218. })
  219. it('receives ipc message in preload script', async () => {
  220. await loadWebView(webview, {
  221. preload: `${fixtures}/module/preload-ipc.js`,
  222. src: `file://${fixtures}/pages/e.html`
  223. })
  224. const message = 'boom!'
  225. webview.send('ping', message)
  226. const { channel, args } = await waitForEvent(webview, 'ipc-message')
  227. expect(channel).to.equal('pong')
  228. expect(args).to.deep.equal([message])
  229. })
  230. it('works without script tag in page', async () => {
  231. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  232. preload: `${fixtures}/module/preload.js`,
  233. src: `file://${fixtures}pages/base-page.html`
  234. })
  235. const types = JSON.parse(message)
  236. expect(types).to.include({
  237. require: 'function',
  238. module: 'object',
  239. process: 'object',
  240. Buffer: 'function'
  241. })
  242. })
  243. it('resolves relative URLs', async () => {
  244. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  245. preload: '../fixtures/module/preload.js',
  246. src: `file://${fixtures}/pages/e.html`
  247. })
  248. const types = JSON.parse(message)
  249. expect(types).to.include({
  250. require: 'function',
  251. module: 'object',
  252. process: 'object',
  253. Buffer: 'function'
  254. })
  255. })
  256. it('ignores empty values', () => {
  257. expect(webview.preload).to.equal('')
  258. for (const emptyValue of ['', null, undefined]) {
  259. webview.preload = emptyValue
  260. expect(webview.preload).to.equal('')
  261. }
  262. })
  263. })
  264. describe('httpreferrer attribute', () => {
  265. it('sets the referrer url', (done) => {
  266. const referrer = 'http://github.com/'
  267. const server = http.createServer((req, res) => {
  268. res.end()
  269. server.close()
  270. expect(req.headers.referer).to.equal(referrer)
  271. done()
  272. }).listen(0, '127.0.0.1', () => {
  273. const port = server.address().port
  274. loadWebView(webview, {
  275. httpreferrer: referrer,
  276. src: `http://127.0.0.1:${port}`
  277. })
  278. })
  279. })
  280. })
  281. describe('useragent attribute', () => {
  282. it('sets the user agent', async () => {
  283. const referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko'
  284. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  285. src: `file://${fixtures}/pages/useragent.html`,
  286. useragent: referrer
  287. })
  288. expect(message).to.equal(referrer)
  289. })
  290. })
  291. describe('disablewebsecurity attribute', () => {
  292. it('does not disable web security when not set', async () => {
  293. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  294. const src = `<script src='file://${jqueryPath}'></script> <script>console.log('ok');</script>`
  295. const encoded = btoa(unescape(encodeURIComponent(src)))
  296. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  297. src: `data:text/html;base64,${encoded}`
  298. })
  299. expect(message).to.be.a('string')
  300. expect(message).to.contain('Not allowed to load local resource')
  301. })
  302. it('disables web security when set', async () => {
  303. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  304. const src = `<script src='file://${jqueryPath}'></script> <script>console.log('ok');</script>`
  305. const encoded = btoa(unescape(encodeURIComponent(src)))
  306. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  307. disablewebsecurity: '',
  308. src: `data:text/html;base64,${encoded}`
  309. })
  310. expect(message).to.equal('ok')
  311. })
  312. it('does not break node integration', async () => {
  313. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  314. disablewebsecurity: '',
  315. nodeintegration: 'on',
  316. src: `file://${fixtures}/pages/d.html`
  317. })
  318. const types = JSON.parse(message)
  319. expect(types).to.include({
  320. require: 'function',
  321. module: 'object',
  322. process: 'object'
  323. })
  324. })
  325. it('does not break preload script', async () => {
  326. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  327. disablewebsecurity: '',
  328. preload: `${fixtures}/module/preload.js`,
  329. src: `file://${fixtures}/pages/e.html`
  330. })
  331. const types = JSON.parse(message)
  332. expect(types).to.include({
  333. require: 'function',
  334. module: 'object',
  335. process: 'object',
  336. Buffer: 'function'
  337. })
  338. })
  339. })
  340. describe('partition attribute', () => {
  341. it('inserts no node symbols when not set', async () => {
  342. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  343. partition: 'test1',
  344. src: `file://${fixtures}/pages/c.html`
  345. })
  346. const types = JSON.parse(message)
  347. expect(types).to.include({
  348. require: 'undefined',
  349. module: 'undefined',
  350. process: 'undefined',
  351. global: 'undefined'
  352. })
  353. })
  354. it('inserts node symbols when set', async () => {
  355. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  356. nodeintegration: 'on',
  357. partition: 'test2',
  358. src: `file://${fixtures}/pages/d.html`
  359. })
  360. const types = JSON.parse(message)
  361. expect(types).to.include({
  362. require: 'function',
  363. module: 'object',
  364. process: 'object'
  365. })
  366. })
  367. it('isolates storage for different id', async () => {
  368. window.localStorage.setItem('test', 'one')
  369. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  370. partition: 'test3',
  371. src: `file://${fixtures}/pages/partition/one.html`
  372. })
  373. const parsedMessage = JSON.parse(message)
  374. expect(parsedMessage).to.include({
  375. numberOfEntries: 0,
  376. testValue: null
  377. })
  378. })
  379. it('uses current session storage when no id is provided', async () => {
  380. const testValue = 'one'
  381. window.localStorage.setItem('test', testValue)
  382. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  383. src: `file://${fixtures}/pages/partition/one.html`
  384. })
  385. const parsedMessage = JSON.parse(message)
  386. expect(parsedMessage).to.include({
  387. numberOfEntries: 1,
  388. testValue
  389. })
  390. })
  391. })
  392. describe('allowpopups attribute', () => {
  393. const generateSpecs = (description, webpreferences = '') => {
  394. describe(description, () => {
  395. it('can not open new window when not set', async () => {
  396. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  397. webpreferences,
  398. src: `file://${fixtures}/pages/window-open-hide.html`
  399. })
  400. expect(message).to.equal('null')
  401. })
  402. it('can open new window when set', async () => {
  403. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  404. webpreferences,
  405. allowpopups: 'on',
  406. src: `file://${fixtures}/pages/window-open-hide.html`
  407. })
  408. expect(message).to.equal('window')
  409. })
  410. })
  411. }
  412. generateSpecs('without sandbox')
  413. generateSpecs('with sandbox', 'sandbox=yes')
  414. generateSpecs('with nativeWindowOpen', 'nativeWindowOpen=yes')
  415. })
  416. describe('webpreferences attribute', () => {
  417. it('can enable nodeintegration', async () => {
  418. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  419. src: `file://${fixtures}/pages/d.html`,
  420. webpreferences: 'nodeIntegration'
  421. })
  422. const types = JSON.parse(message)
  423. expect(types).to.include({
  424. require: 'function',
  425. module: 'object',
  426. process: 'object'
  427. })
  428. })
  429. it('can disable the remote module', async () => {
  430. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  431. preload: `${fixtures}/module/preload-disable-remote.js`,
  432. src: `file://${fixtures}/api/blank.html`,
  433. webpreferences: 'enableRemoteModule=no'
  434. })
  435. const typeOfRemote = JSON.parse(message)
  436. expect(typeOfRemote).to.equal('undefined')
  437. })
  438. it('can disables web security and enable nodeintegration', async () => {
  439. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  440. const src = `<script src='file://${jqueryPath}'></script> <script>console.log(typeof require);</script>`
  441. const encoded = btoa(unescape(encodeURIComponent(src)))
  442. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  443. src: `data:text/html;base64,${encoded}`,
  444. webpreferences: 'webSecurity=no, nodeIntegration=yes'
  445. })
  446. expect(message).to.equal('function')
  447. })
  448. })
  449. describe('new-window event', () => {
  450. it('emits when window.open is called', async () => {
  451. loadWebView(webview, {
  452. src: `file://${fixtures}/pages/window-open.html`
  453. })
  454. const { url, frameName } = await waitForEvent(webview, 'new-window')
  455. expect(url).to.equal('http://host/')
  456. expect(frameName).to.equal('host')
  457. })
  458. it('emits when link with target is called', async () => {
  459. loadWebView(webview, {
  460. src: `file://${fixtures}/pages/target-name.html`
  461. })
  462. const { url, frameName } = await waitForEvent(webview, 'new-window')
  463. expect(url).to.equal('http://host/')
  464. expect(frameName).to.equal('target')
  465. })
  466. })
  467. describe('ipc-message event', () => {
  468. it('emits when guest sends an ipc message to browser', async () => {
  469. loadWebView(webview, {
  470. nodeintegration: 'on',
  471. src: `file://${fixtures}/pages/ipc-message.html`
  472. })
  473. const { channel, args } = await waitForEvent(webview, 'ipc-message')
  474. expect(channel).to.equal('channel')
  475. expect(args).to.deep.equal(['arg1', 'arg2'])
  476. })
  477. })
  478. describe('page-title-set event', () => {
  479. it('emits when title is set', async () => {
  480. loadWebView(webview, {
  481. src: `file://${fixtures}/pages/a.html`
  482. })
  483. const { title, explicitSet } = await waitForEvent(webview, 'page-title-set')
  484. expect(title).to.equal('test')
  485. expect(explicitSet).to.be.true()
  486. })
  487. })
  488. describe('page-favicon-updated event', () => {
  489. it('emits when favicon urls are received', async () => {
  490. loadWebView(webview, {
  491. src: `file://${fixtures}/pages/a.html`
  492. })
  493. const { favicons } = await waitForEvent(webview, 'page-favicon-updated')
  494. expect(favicons).to.be.an('array').of.length(2)
  495. if (process.platform === 'win32') {
  496. expect(favicons[0]).to.match(/^file:\/\/\/[A-Z]:\/favicon.png$/i)
  497. } else {
  498. expect(favicons[0]).to.equal('file:///favicon.png')
  499. }
  500. })
  501. })
  502. describe('will-navigate event', () => {
  503. it('emits when a url that leads to oustide of the page is clicked', async () => {
  504. loadWebView(webview, {
  505. src: `file://${fixtures}/pages/webview-will-navigate.html`
  506. })
  507. const { url } = await waitForEvent(webview, 'will-navigate')
  508. expect(url).to.equal('http://host/')
  509. })
  510. })
  511. describe('did-navigate event', () => {
  512. let p = path.join(fixtures, 'pages', 'webview-will-navigate.html')
  513. p = p.replace(/\\/g, '/')
  514. const pageUrl = url.format({
  515. protocol: 'file',
  516. slashes: true,
  517. pathname: p
  518. })
  519. it('emits when a url that leads to outside of the page is clicked', async () => {
  520. loadWebView(webview, { src: pageUrl })
  521. const { url } = await waitForEvent(webview, 'did-navigate')
  522. expect(url).to.equal(pageUrl)
  523. })
  524. })
  525. describe('did-navigate-in-page event', () => {
  526. it('emits when an anchor link is clicked', async () => {
  527. let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html')
  528. p = p.replace(/\\/g, '/')
  529. const pageUrl = url.format({
  530. protocol: 'file',
  531. slashes: true,
  532. pathname: p
  533. })
  534. loadWebView(webview, { src: pageUrl })
  535. const event = await waitForEvent(webview, 'did-navigate-in-page')
  536. expect(event.url).to.equal(`${pageUrl}#test_content`)
  537. })
  538. it('emits when window.history.replaceState is called', async () => {
  539. loadWebView(webview, {
  540. src: `file://${fixtures}/pages/webview-did-navigate-in-page-with-history.html`
  541. })
  542. const { url } = await waitForEvent(webview, 'did-navigate-in-page')
  543. expect(url).to.equal('http://host/')
  544. })
  545. it('emits when window.location.hash is changed', async () => {
  546. let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html')
  547. p = p.replace(/\\/g, '/')
  548. const pageUrl = url.format({
  549. protocol: 'file',
  550. slashes: true,
  551. pathname: p
  552. })
  553. loadWebView(webview, { src: pageUrl })
  554. const event = await waitForEvent(webview, 'did-navigate-in-page')
  555. expect(event.url).to.equal(`${pageUrl}#test`)
  556. })
  557. })
  558. describe('close event', () => {
  559. it('should fire when interior page calls window.close', async () => {
  560. loadWebView(webview, { src: `file://${fixtures}/pages/close.html` })
  561. await waitForEvent(webview, 'close')
  562. })
  563. })
  564. // FIXME(zcbenz): Disabled because of moving to OOPIF webview.
  565. xdescribe('setDevToolsWebContents() API', () => {
  566. it('sets webContents of webview as devtools', async () => {
  567. const webview2 = new WebView()
  568. loadWebView(webview2)
  569. // Setup an event handler for further usage.
  570. const waitForDomReady = waitForEvent(webview2, 'dom-ready')
  571. loadWebView(webview, { src: 'about:blank' })
  572. await waitForEvent(webview, 'dom-ready')
  573. webview.getWebContents().setDevToolsWebContents(webview2.getWebContents())
  574. webview.getWebContents().openDevTools()
  575. await waitForDomReady
  576. // Its WebContents should be a DevTools.
  577. const devtools = webview2.getWebContents()
  578. expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true()
  579. const name = await devtools.executeJavaScript('InspectorFrontendHost.constructor.name')
  580. document.body.removeChild(webview2)
  581. expect(name).to.be.equal('InspectorFrontendHostImpl')
  582. })
  583. })
  584. describe('devtools-opened event', () => {
  585. it('should fire when webview.openDevTools() is called', async () => {
  586. loadWebView(webview, {
  587. src: `file://${fixtures}/pages/base-page.html`
  588. })
  589. await waitForEvent(webview, 'dom-ready')
  590. webview.openDevTools()
  591. await waitForEvent(webview, 'devtools-opened')
  592. webview.closeDevTools()
  593. })
  594. })
  595. describe('devtools-closed event', () => {
  596. it('should fire when webview.closeDevTools() is called', async () => {
  597. loadWebView(webview, {
  598. src: `file://${fixtures}/pages/base-page.html`
  599. })
  600. await waitForEvent(webview, 'dom-ready')
  601. webview.openDevTools()
  602. await waitForEvent(webview, 'devtools-opened')
  603. webview.closeDevTools()
  604. await waitForEvent(webview, 'devtools-closed')
  605. })
  606. })
  607. describe('devtools-focused event', () => {
  608. it('should fire when webview.openDevTools() is called', async () => {
  609. loadWebView(webview, {
  610. src: `file://${fixtures}/pages/base-page.html`
  611. })
  612. const waitForDevToolsFocused = waitForEvent(webview, 'devtools-focused')
  613. await waitForEvent(webview, 'dom-ready')
  614. webview.openDevTools()
  615. await waitForDevToolsFocused
  616. webview.closeDevTools()
  617. })
  618. })
  619. describe('<webview>.reload()', () => {
  620. it('should emit beforeunload handler', async () => {
  621. await loadWebView(webview, {
  622. nodeintegration: 'on',
  623. src: `file://${fixtures}/pages/beforeunload-false.html`
  624. })
  625. // Event handler has to be added before reload.
  626. const waitForOnbeforeunload = waitForEvent(webview, 'ipc-message')
  627. webview.reload()
  628. const { channel } = await waitForOnbeforeunload
  629. expect(channel).to.equal('onbeforeunload')
  630. })
  631. })
  632. describe('<webview>.goForward()', () => {
  633. it('should work after a replaced history entry', (done) => {
  634. let loadCount = 1
  635. const listener = (e) => {
  636. if (loadCount === 1) {
  637. expect(e.channel).to.equal('history')
  638. expect(e.args[0]).to.equal(1)
  639. expect(webview.canGoBack()).to.be.false()
  640. expect(webview.canGoForward()).to.be.false()
  641. } else if (loadCount === 2) {
  642. expect(e.channel).to.equal('history')
  643. expect(e.args[0]).to.equal(2)
  644. expect(webview.canGoBack()).to.be.false()
  645. expect(webview.canGoForward()).to.be.true()
  646. webview.removeEventListener('ipc-message', listener)
  647. }
  648. }
  649. const loadListener = () => {
  650. if (loadCount === 1) {
  651. webview.src = `file://${fixtures}/pages/base-page.html`
  652. } else if (loadCount === 2) {
  653. expect(webview.canGoBack()).to.be.true()
  654. expect(webview.canGoForward()).to.be.false()
  655. webview.goBack()
  656. } else if (loadCount === 3) {
  657. webview.goForward()
  658. } else if (loadCount === 4) {
  659. expect(webview.canGoBack()).to.be.true()
  660. expect(webview.canGoForward()).to.be.false()
  661. webview.removeEventListener('did-finish-load', loadListener)
  662. done()
  663. }
  664. loadCount += 1
  665. }
  666. webview.addEventListener('ipc-message', listener)
  667. webview.addEventListener('did-finish-load', loadListener)
  668. loadWebView(webview, {
  669. nodeintegration: 'on',
  670. src: `file://${fixtures}/pages/history-replace.html`
  671. })
  672. })
  673. })
  674. // FIXME: https://github.com/electron/electron/issues/19397
  675. xdescribe('<webview>.clearHistory()', () => {
  676. it('should clear the navigation history', async () => {
  677. const message = waitForEvent(webview, 'ipc-message')
  678. await loadWebView(webview, {
  679. nodeintegration: 'on',
  680. src: `file://${fixtures}/pages/history.html`
  681. })
  682. const event = await message
  683. expect(event.channel).to.equal('history')
  684. expect(event.args[0]).to.equal(2)
  685. expect(webview.canGoBack()).to.be.true()
  686. webview.clearHistory()
  687. expect(webview.canGoBack()).to.be.false()
  688. })
  689. })
  690. describe('basic auth', () => {
  691. const auth = require('basic-auth')
  692. it('should authenticate with correct credentials', (done) => {
  693. const message = 'Authenticated'
  694. const server = http.createServer((req, res) => {
  695. const credentials = auth(req)
  696. if (credentials.name === 'test' && credentials.pass === 'test') {
  697. res.end(message)
  698. } else {
  699. res.end('failed')
  700. }
  701. server.close()
  702. })
  703. server.listen(0, '127.0.0.1', () => {
  704. const port = server.address().port
  705. webview.addEventListener('ipc-message', (e) => {
  706. expect(e.channel).to.equal(message)
  707. done()
  708. })
  709. loadWebView(webview, {
  710. nodeintegration: 'on',
  711. src: `file://${fixtures}/pages/basic-auth.html?port=${port}`
  712. })
  713. })
  714. })
  715. })
  716. describe('dom-ready event', () => {
  717. it('emits when document is loaded', (done) => {
  718. const server = http.createServer(() => {})
  719. server.listen(0, '127.0.0.1', () => {
  720. const port = server.address().port
  721. webview.addEventListener('dom-ready', () => {
  722. done()
  723. })
  724. loadWebView(webview, {
  725. src: `file://${fixtures}/pages/dom-ready.html?port=${port}`
  726. })
  727. })
  728. })
  729. it('throws a custom error when an API method is called before the event is emitted', () => {
  730. const expectedErrorMessage =
  731. 'The WebView must be attached to the DOM ' +
  732. 'and the dom-ready event emitted before this method can be called.'
  733. expect(() => { webview.stop() }).to.throw(expectedErrorMessage)
  734. })
  735. })
  736. describe('executeJavaScript', () => {
  737. it('should support user gesture', async () => {
  738. await loadWebView(webview, {
  739. src: `file://${fixtures}/pages/fullscreen.html`
  740. })
  741. // Event handler has to be added before js execution.
  742. const waitForEnterHtmlFullScreen = waitForEvent(webview, 'enter-html-full-screen')
  743. const jsScript = "document.querySelector('video').webkitRequestFullscreen()"
  744. webview.executeJavaScript(jsScript, true)
  745. return waitForEnterHtmlFullScreen
  746. })
  747. it('can return the result of the executed script', async () => {
  748. await loadWebView(webview, {
  749. src: 'about:blank'
  750. })
  751. const jsScript = "'4'+2"
  752. const expectedResult = '42'
  753. const result = await webview.executeJavaScript(jsScript)
  754. expect(result).to.equal(expectedResult)
  755. })
  756. })
  757. it('supports inserting CSS', async () => {
  758. await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` })
  759. await webview.insertCSS('body { background-repeat: round; }')
  760. const result = await webview.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
  761. expect(result).to.equal('round')
  762. })
  763. it('supports removing inserted CSS', async () => {
  764. await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` })
  765. const key = await webview.insertCSS('body { background-repeat: round; }')
  766. await webview.removeInsertedCSS(key)
  767. const result = await webview.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")')
  768. expect(result).to.equal('repeat')
  769. })
  770. describe('sendInputEvent', () => {
  771. it('can send keyboard event', async () => {
  772. loadWebView(webview, {
  773. nodeintegration: 'on',
  774. src: `file://${fixtures}/pages/onkeyup.html`
  775. })
  776. await waitForEvent(webview, 'dom-ready')
  777. const waitForIpcMessage = waitForEvent(webview, 'ipc-message')
  778. webview.sendInputEvent({
  779. type: 'keyup',
  780. keyCode: 'c',
  781. modifiers: ['shift']
  782. })
  783. const { channel, args } = await waitForIpcMessage
  784. expect(channel).to.equal('keyup')
  785. expect(args).to.deep.equal(['C', 'KeyC', 67, true, false])
  786. })
  787. it('can send mouse event', async () => {
  788. loadWebView(webview, {
  789. nodeintegration: 'on',
  790. src: `file://${fixtures}/pages/onmouseup.html`
  791. })
  792. await waitForEvent(webview, 'dom-ready')
  793. const waitForIpcMessage = waitForEvent(webview, 'ipc-message')
  794. webview.sendInputEvent({
  795. type: 'mouseup',
  796. modifiers: ['ctrl'],
  797. x: 10,
  798. y: 20
  799. })
  800. const { channel, args } = await waitForIpcMessage
  801. expect(channel).to.equal('mouseup')
  802. expect(args).to.deep.equal([10, 20, false, true])
  803. })
  804. })
  805. describe('media-started-playing media-paused events', () => {
  806. it('emits when audio starts and stops playing', async () => {
  807. await loadWebView(webview, { src: `file://${fixtures}/pages/base-page.html` })
  808. // With the new autoplay policy, audio elements must be unmuted
  809. // see https://goo.gl/xX8pDD.
  810. const source = `
  811. const audio = document.createElement("audio")
  812. audio.src = "../assets/tone.wav"
  813. document.body.appendChild(audio);
  814. audio.play()
  815. `
  816. webview.executeJavaScript(source, true)
  817. await waitForEvent(webview, 'media-started-playing')
  818. webview.executeJavaScript('document.querySelector("audio").pause()', true)
  819. await waitForEvent(webview, 'media-paused')
  820. })
  821. })
  822. describe('found-in-page event', () => {
  823. it('emits when a request is made', (done) => {
  824. let requestId = null
  825. const activeMatchOrdinal = []
  826. const listener = (e) => {
  827. expect(e.result.requestId).to.equal(requestId)
  828. expect(e.result.matches).to.equal(3)
  829. activeMatchOrdinal.push(e.result.activeMatchOrdinal)
  830. if (e.result.activeMatchOrdinal === e.result.matches) {
  831. expect(activeMatchOrdinal).to.deep.equal([1, 2, 3])
  832. webview.stopFindInPage('clearSelection')
  833. done()
  834. } else {
  835. listener2()
  836. }
  837. }
  838. const listener2 = () => {
  839. requestId = webview.findInPage('virtual')
  840. }
  841. webview.addEventListener('found-in-page', listener)
  842. webview.addEventListener('did-finish-load', listener2)
  843. loadWebView(webview, { src: `file://${fixtures}/pages/content.html` })
  844. // TODO(deepak1556): With https://codereview.chromium.org/2836973002
  845. // focus of the webContents is required when triggering the api.
  846. // Remove this workaround after determining the cause for
  847. // incorrect focus.
  848. webview.focus()
  849. })
  850. })
  851. describe('did-change-theme-color event', () => {
  852. it('emits when theme color changes', async () => {
  853. loadWebView(webview, {
  854. src: `file://${fixtures}/pages/theme-color.html`
  855. })
  856. await waitForEvent(webview, 'did-change-theme-color')
  857. })
  858. })
  859. describe('<webview>.getWebContentsId', () => {
  860. it('can return the WebContents ID', async () => {
  861. const src = 'about:blank'
  862. await loadWebView(webview, { src })
  863. expect(webview.getWebContentsId()).to.be.a('number')
  864. })
  865. })
  866. describe('<webview>.capturePage()', () => {
  867. before(function () {
  868. // TODO(miniak): figure out why this is failing on windows
  869. if (process.platform === 'win32') {
  870. this.skip()
  871. }
  872. })
  873. it('returns a Promise with a NativeImage', async () => {
  874. const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'
  875. await loadWebView(webview, { src })
  876. const image = await webview.capturePage()
  877. const imgBuffer = image.toPNG()
  878. // Check the 25th byte in the PNG.
  879. // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha
  880. expect(imgBuffer[25]).to.equal(6)
  881. })
  882. })
  883. describe('<webview>.printToPDF()', () => {
  884. before(function () {
  885. if (!features.isPrintingEnabled()) {
  886. this.skip()
  887. }
  888. })
  889. it('can print to PDF', async () => {
  890. const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'
  891. await loadWebView(webview, { src })
  892. const data = await webview.printToPDF({})
  893. expect(data).to.be.an.instanceof(Uint8Array).that.is.not.empty()
  894. })
  895. })
  896. describe('will-attach-webview event', () => {
  897. it('does not emit when src is not changed', (done) => {
  898. loadWebView(webview)
  899. setTimeout(() => {
  900. const expectedErrorMessage =
  901. 'The WebView must be attached to the DOM ' +
  902. 'and the dom-ready event emitted before this method can be called.'
  903. expect(() => { webview.stop() }).to.throw(expectedErrorMessage)
  904. done()
  905. })
  906. })
  907. it('supports changing the web preferences', async () => {
  908. ipcRenderer.send('disable-node-on-next-will-attach-webview')
  909. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  910. nodeintegration: 'yes',
  911. src: `file://${fixtures}/pages/a.html`
  912. })
  913. const types = JSON.parse(message)
  914. expect(types).to.include({
  915. require: 'undefined',
  916. module: 'undefined',
  917. process: 'undefined',
  918. global: 'undefined'
  919. })
  920. })
  921. it('supports preventing a webview from being created', async () => {
  922. ipcRenderer.send('prevent-next-will-attach-webview')
  923. loadWebView(webview, {
  924. src: `file://${fixtures}/pages/c.html`
  925. })
  926. await waitForEvent(webview, 'destroyed')
  927. })
  928. it('supports removing the preload script', async () => {
  929. ipcRenderer.send('disable-preload-on-next-will-attach-webview')
  930. const message = await startLoadingWebViewAndWaitForMessage(webview, {
  931. nodeintegration: 'yes',
  932. preload: path.join(fixtures, 'module', 'preload-set-global.js'),
  933. src: `file://${fixtures}/pages/a.html`
  934. })
  935. expect(message).to.equal('undefined')
  936. })
  937. })
  938. describe('DOM events', () => {
  939. let div
  940. beforeEach(() => {
  941. div = document.createElement('div')
  942. div.style.width = '100px'
  943. div.style.height = '10px'
  944. div.style.overflow = 'hidden'
  945. webview.style.height = '100%'
  946. webview.style.width = '100%'
  947. })
  948. afterEach(() => {
  949. if (div != null) div.remove()
  950. })
  951. const generateSpecs = (description, sandbox) => {
  952. describe(description, () => {
  953. // TODO(nornagon): disabled during chromium roll 2019-06-11 due to a
  954. // 'ResizeObserver loop limit exceeded' error on Windows
  955. xit('emits resize events', async () => {
  956. const firstResizeSignal = waitForEvent(webview, 'resize')
  957. const domReadySignal = waitForEvent(webview, 'dom-ready')
  958. webview.src = `file://${fixtures}/pages/a.html`
  959. webview.webpreferences = `sandbox=${sandbox ? 'yes' : 'no'}`
  960. div.appendChild(webview)
  961. document.body.appendChild(div)
  962. const firstResizeEvent = await firstResizeSignal
  963. expect(firstResizeEvent.target).to.equal(webview)
  964. expect(firstResizeEvent.newWidth).to.equal(100)
  965. expect(firstResizeEvent.newHeight).to.equal(10)
  966. await domReadySignal
  967. const secondResizeSignal = waitForEvent(webview, 'resize')
  968. const newWidth = 1234
  969. const newHeight = 789
  970. div.style.width = `${newWidth}px`
  971. div.style.height = `${newHeight}px`
  972. const secondResizeEvent = await secondResizeSignal
  973. expect(secondResizeEvent.target).to.equal(webview)
  974. expect(secondResizeEvent.newWidth).to.equal(newWidth)
  975. expect(secondResizeEvent.newHeight).to.equal(newHeight)
  976. })
  977. it('emits focus event', async () => {
  978. const domReadySignal = waitForEvent(webview, 'dom-ready')
  979. webview.src = `file://${fixtures}/pages/a.html`
  980. webview.webpreferences = `sandbox=${sandbox ? 'yes' : 'no'}`
  981. document.body.appendChild(webview)
  982. await domReadySignal
  983. // If this test fails, check if webview.focus() still works.
  984. const focusSignal = waitForEvent(webview, 'focus')
  985. webview.focus()
  986. await focusSignal
  987. })
  988. })
  989. }
  990. generateSpecs('without sandbox', false)
  991. generateSpecs('with sandbox', true)
  992. })
  993. })