webview-spec.js 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809
  1. const assert = require('assert')
  2. const {expect} = require('chai')
  3. const path = require('path')
  4. const http = require('http')
  5. const url = require('url')
  6. const {ipcRenderer, remote} = require('electron')
  7. const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = remote
  8. const {closeWindow} = require('./window-helpers')
  9. const isCI = remote.getGlobal('isCi')
  10. const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled')
  11. /* Most of the APIs here don't use standard callbacks */
  12. /* eslint-disable standard/no-callback-literal */
  13. describe('<webview> tag', function () {
  14. this.timeout(3 * 60 * 1000)
  15. const fixtures = path.join(__dirname, 'fixtures')
  16. let webview = null
  17. let w = null
  18. const openTheWindow = async (...args) => {
  19. await closeTheWindow()
  20. w = new BrowserWindow(...args)
  21. return w
  22. }
  23. const closeTheWindow = async () => {
  24. await closeWindow(w)
  25. w = null
  26. }
  27. const waitForEvent = (eventTarget, eventName) => {
  28. return new Promise((resolve) => {
  29. eventTarget.addEventListener(eventName, resolve, {once: true})
  30. })
  31. }
  32. const waitForOnce = (eventTarget, eventName) => {
  33. return new Promise((resolve) => {
  34. eventTarget.once(eventName, (...args) => resolve(args))
  35. })
  36. }
  37. const loadWebView = (webview, attributes = {}) => {
  38. for (const [name, value] of Object.entries(attributes)) {
  39. webview.setAttribute(name, value)
  40. }
  41. document.body.appendChild(webview)
  42. return waitForEvent(webview, 'did-finish-load')
  43. }
  44. const startLoadingWebViewAndWaitForMessage = (webview, attributes = {}) => {
  45. loadWebView(webview, attributes) // Don't wait for load to be finished.
  46. return waitForEvent(webview, 'console-message')
  47. }
  48. beforeEach(() => {
  49. webview = new WebView()
  50. })
  51. afterEach(() => {
  52. if (!document.body.contains(webview)) {
  53. document.body.appendChild(webview)
  54. }
  55. webview.remove()
  56. return closeTheWindow()
  57. })
  58. it('works without script tag in page', async () => {
  59. const w = await openTheWindow({show: false})
  60. w.loadURL('file://' + fixtures + '/pages/webview-no-script.html')
  61. await waitForOnce(ipcMain, 'pong')
  62. })
  63. it('is disabled when nodeIntegration is disabled', async () => {
  64. const w = await openTheWindow({
  65. show: false,
  66. webPreferences: {
  67. nodeIntegration: false,
  68. preload: path.join(fixtures, 'module', 'preload-webview.js')
  69. }
  70. })
  71. w.loadURL(`file://${fixtures}/pages/webview-no-script.html`)
  72. const [, type] = await waitForOnce(ipcMain, 'webview')
  73. expect(type).to.equal('undefined', 'WebView still exists')
  74. })
  75. it('is enabled when the webviewTag option is enabled and the nodeIntegration option is disabled', async () => {
  76. const w = await openTheWindow({
  77. show: false,
  78. webPreferences: {
  79. nodeIntegration: false,
  80. preload: path.join(fixtures, 'module', 'preload-webview.js'),
  81. webviewTag: true
  82. }
  83. })
  84. w.loadURL(`file://${fixtures}/pages/webview-no-script.html`)
  85. const [, type] = await waitForOnce(ipcMain, 'webview')
  86. expect(type).to.not.equal('undefined', 'WebView is not created')
  87. })
  88. describe('src attribute', () => {
  89. it('specifies the page to load', async () => {
  90. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  91. src: `file://${fixtures}/pages/a.html`
  92. })
  93. expect(message).to.equal('a')
  94. })
  95. it('navigates to new page when changed', async () => {
  96. await loadWebView(webview, {
  97. src: `file://${fixtures}/pages/a.html`
  98. })
  99. webview.src = `file://${fixtures}/pages/b.html`
  100. const {message} = await waitForEvent(webview, 'console-message')
  101. expect(message).to.equal('b')
  102. })
  103. it('resolves relative URLs', async () => {
  104. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  105. src: '../fixtures/pages/e.html'
  106. })
  107. assert.equal(message, 'Window script is loaded before preload script')
  108. })
  109. it('ignores empty values', () => {
  110. assert.equal(webview.src, '')
  111. for (const emptyValue of ['', null, undefined]) {
  112. webview.src = emptyValue
  113. expect(webview.src).to.equal('')
  114. }
  115. })
  116. })
  117. describe('nodeintegration attribute', () => {
  118. it('inserts no node symbols when not set', async () => {
  119. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  120. src: `file://${fixtures}/pages/c.html`
  121. })
  122. const types = JSON.parse(message)
  123. expect(types).to.include({
  124. require: 'undefined',
  125. module: 'undefined',
  126. process: 'undefined',
  127. global: 'undefined'
  128. })
  129. })
  130. it('inserts node symbols when set', async () => {
  131. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  132. nodeintegration: 'on',
  133. src: `file://${fixtures}/pages/d.html`
  134. })
  135. const types = JSON.parse(message)
  136. expect(types).to.include({
  137. require: 'function',
  138. module: 'object',
  139. process: 'object'
  140. })
  141. })
  142. it('loads node symbols after POST navigation when set', async function () {
  143. // FIXME Figure out why this is timing out on AppVeyor
  144. if (process.env.APPVEYOR === 'True') {
  145. this.skip()
  146. return
  147. }
  148. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  149. nodeintegration: 'on',
  150. src: `file://${fixtures}/pages/post.html`
  151. })
  152. const types = JSON.parse(message)
  153. expect(types).to.include({
  154. require: 'function',
  155. module: 'object',
  156. process: 'object'
  157. })
  158. })
  159. it('disables node integration on child windows when it is disabled on the webview', (done) => {
  160. app.once('browser-window-created', (event, window) => {
  161. assert.equal(window.webContents.getWebPreferences().nodeIntegration, false)
  162. done()
  163. })
  164. const src = url.format({
  165. pathname: `${fixtures}/pages/webview-opener-no-node-integration.html`,
  166. protocol: 'file',
  167. query: {
  168. p: `${fixtures}/pages/window-opener-node.html`
  169. },
  170. slashes: true
  171. })
  172. loadWebView(webview, {
  173. allowpopups: 'on',
  174. src
  175. })
  176. })
  177. it('loads native modules when navigation happens', async function () {
  178. if (!nativeModulesEnabled) {
  179. this.skip()
  180. return
  181. }
  182. await loadWebView(webview, {
  183. nodeintegration: 'on',
  184. src: `file://${fixtures}/pages/native-module.html`
  185. })
  186. webview.reload()
  187. const {message} = await waitForEvent(webview, 'console-message')
  188. assert.equal(message, 'function')
  189. })
  190. })
  191. describe('preload attribute', () => {
  192. it('loads the script before other scripts in window', async () => {
  193. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  194. preload: `${fixtures}/module/preload.js`,
  195. src: `file://${fixtures}/pages/e.html`
  196. })
  197. expect(message).to.be.a('string')
  198. expect(message).to.be.not.equal('Window script is loaded before preload script')
  199. })
  200. it('preload script can still use "process" and "Buffer" when nodeintegration is off', async () => {
  201. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  202. preload: `${fixtures}/module/preload-node-off.js`,
  203. src: `file://${fixtures}/api/blank.html`
  204. })
  205. const types = JSON.parse(message)
  206. expect(types).to.include({
  207. process: 'object',
  208. Buffer: 'function'
  209. })
  210. })
  211. it('preload script can require modules that still use "process" and "Buffer" when nodeintegration is off', async () => {
  212. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  213. preload: `${fixtures}/module/preload-node-off-wrapper.js`,
  214. src: `file://${fixtures}/api/blank.html`
  215. })
  216. const types = JSON.parse(message)
  217. expect(types).to.include({
  218. process: 'object',
  219. Buffer: 'function'
  220. })
  221. })
  222. it('receives ipc message in preload script', async () => {
  223. await loadWebView(webview, {
  224. preload: `${fixtures}/module/preload-ipc.js`,
  225. src: `file://${fixtures}/pages/e.html`
  226. })
  227. const message = 'boom!'
  228. webview.send('ping', message)
  229. const {channel, args} = await waitForEvent(webview, 'ipc-message')
  230. assert.equal(channel, 'pong')
  231. assert.deepEqual(args, [message])
  232. })
  233. it('works without script tag in page', async () => {
  234. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  235. preload: `${fixtures}/module/preload.js`,
  236. src: `file://${fixtures}pages/base-page.html`
  237. })
  238. const types = JSON.parse(message)
  239. expect(types).to.include({
  240. require: 'function',
  241. module: 'object',
  242. process: 'object',
  243. Buffer: 'function'
  244. })
  245. })
  246. it('resolves relative URLs', async () => {
  247. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  248. preload: '../fixtures/module/preload.js',
  249. src: `file://${fixtures}/pages/e.html`
  250. })
  251. const types = JSON.parse(message)
  252. expect(types).to.include({
  253. require: 'function',
  254. module: 'object',
  255. process: 'object',
  256. Buffer: 'function'
  257. })
  258. })
  259. it('ignores empty values', () => {
  260. assert.equal(webview.preload, '')
  261. for (const emptyValue of ['', null, undefined]) {
  262. webview.preload = emptyValue
  263. assert.equal(webview.preload, '')
  264. }
  265. })
  266. })
  267. describe('httpreferrer attribute', () => {
  268. it('sets the referrer url', (done) => {
  269. const referrer = 'http://github.com/'
  270. const server = http.createServer((req, res) => {
  271. res.end()
  272. server.close()
  273. assert.equal(req.headers.referer, referrer)
  274. done()
  275. }).listen(0, '127.0.0.1', () => {
  276. const port = server.address().port
  277. loadWebView(webview, {
  278. httpreferrer: referrer,
  279. src: `http://127.0.0.1:${port}`
  280. })
  281. })
  282. })
  283. })
  284. describe('useragent attribute', () => {
  285. it('sets the user agent', async () => {
  286. const referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko'
  287. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  288. src: `file://${fixtures}/pages/useragent.html`,
  289. useragent: referrer
  290. })
  291. expect(message).to.equal(referrer)
  292. })
  293. })
  294. describe('disablewebsecurity attribute', () => {
  295. it('does not disable web security when not set', async () => {
  296. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  297. const src = `<script src='file://${jqueryPath}'></script> <script>console.log('ok');</script>`
  298. const encoded = btoa(unescape(encodeURIComponent(src)))
  299. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  300. src: `data:text/html;base64,${encoded}`
  301. })
  302. expect(message).to.be.a('string')
  303. expect(message).to.contain('Not allowed to load local resource')
  304. })
  305. it('disables web security when set', async () => {
  306. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  307. const src = `<script src='file://${jqueryPath}'></script> <script>console.log('ok');</script>`
  308. const encoded = btoa(unescape(encodeURIComponent(src)))
  309. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  310. disablewebsecurity: '',
  311. src: `data:text/html;base64,${encoded}`
  312. })
  313. expect(message).to.equal('ok')
  314. })
  315. it('does not break node integration', async () => {
  316. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  317. disablewebsecurity: '',
  318. nodeintegration: 'on',
  319. src: `file://${fixtures}/pages/d.html`
  320. })
  321. const types = JSON.parse(message)
  322. expect(types).to.include({
  323. require: 'function',
  324. module: 'object',
  325. process: 'object'
  326. })
  327. })
  328. it('does not break preload script', async () => {
  329. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  330. disablewebsecurity: '',
  331. preload: `${fixtures}/module/preload.js`,
  332. src: `file://${fixtures}/pages/e.html`
  333. })
  334. const types = JSON.parse(message)
  335. expect(types).to.include({
  336. require: 'function',
  337. module: 'object',
  338. process: 'object',
  339. Buffer: 'function'
  340. })
  341. })
  342. })
  343. describe('partition attribute', () => {
  344. it('inserts no node symbols when not set', async () => {
  345. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  346. partition: 'test1',
  347. src: `file://${fixtures}/pages/c.html`
  348. })
  349. const types = JSON.parse(message)
  350. expect(types).to.include({
  351. require: 'undefined',
  352. module: 'undefined',
  353. process: 'undefined',
  354. global: 'undefined'
  355. })
  356. })
  357. it('inserts node symbols when set', async () => {
  358. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  359. nodeintegration: 'on',
  360. partition: 'test2',
  361. src: `file://${fixtures}/pages/d.html`
  362. })
  363. const types = JSON.parse(message)
  364. expect(types).to.include({
  365. require: 'function',
  366. module: 'object',
  367. process: 'object'
  368. })
  369. })
  370. it('isolates storage for different id', async () => {
  371. window.localStorage.setItem('test', 'one')
  372. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  373. partition: 'test3',
  374. src: `file://${fixtures}/pages/partition/one.html`
  375. })
  376. const parsedMessage = JSON.parse(message)
  377. expect(parsedMessage).to.include({
  378. numberOfEntries: 0,
  379. testValue: null
  380. })
  381. })
  382. it('uses current session storage when no id is provided', async () => {
  383. const testValue = 'one'
  384. window.localStorage.setItem('test', testValue)
  385. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  386. src: `file://${fixtures}/pages/partition/one.html`
  387. })
  388. const parsedMessage = JSON.parse(message)
  389. expect(parsedMessage).to.include({
  390. numberOfEntries: 1,
  391. testValue
  392. })
  393. })
  394. })
  395. describe('allowpopups attribute', () => {
  396. it('can not open new window when not set', async () => {
  397. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  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. allowpopups: 'on',
  405. src: `file://${fixtures}/pages/window-open-hide.html`
  406. })
  407. expect(message).to.equal('window')
  408. })
  409. })
  410. describe('webpreferences attribute', () => {
  411. it('can enable nodeintegration', async () => {
  412. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  413. src: `file://${fixtures}/pages/d.html`,
  414. webpreferences: 'nodeIntegration'
  415. })
  416. const types = JSON.parse(message)
  417. expect(types).to.include({
  418. require: 'function',
  419. module: 'object',
  420. process: 'object'
  421. })
  422. })
  423. it('can disables web security and enable nodeintegration', async () => {
  424. const jqueryPath = path.join(__dirname, '/static/jquery-2.0.3.min.js')
  425. const src = `<script src='file://${jqueryPath}'></script> <script>console.log(typeof require);</script>`
  426. const encoded = btoa(unescape(encodeURIComponent(src)))
  427. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  428. src: `data:text/html;base64,${encoded}`,
  429. webpreferences: 'webSecurity=no, nodeIntegration=yes'
  430. })
  431. expect(message).to.equal('function')
  432. })
  433. it('can enable context isolation', async () => {
  434. loadWebView(webview, {
  435. allowpopups: 'yes',
  436. preload: path.join(fixtures, 'api', 'isolated-preload.js'),
  437. src: `file://${fixtures}/api/isolated.html`,
  438. webpreferences: 'contextIsolation=yes'
  439. })
  440. const [, data] = await waitForOnce(ipcMain, 'isolated-world')
  441. assert.deepEqual(data, {
  442. preloadContext: {
  443. preloadProperty: 'number',
  444. pageProperty: 'undefined',
  445. typeofRequire: 'function',
  446. typeofProcess: 'object',
  447. typeofArrayPush: 'function',
  448. typeofFunctionApply: 'function'
  449. },
  450. pageContext: {
  451. preloadProperty: 'undefined',
  452. pageProperty: 'string',
  453. typeofRequire: 'undefined',
  454. typeofProcess: 'undefined',
  455. typeofArrayPush: 'number',
  456. typeofFunctionApply: 'boolean',
  457. typeofPreloadExecuteJavaScriptProperty: 'number',
  458. typeofOpenedWindow: 'object'
  459. }
  460. })
  461. })
  462. })
  463. describe('new-window event', () => {
  464. it('emits when window.open is called', async () => {
  465. loadWebView(webview, {
  466. src: `file://${fixtures}/pages/window-open.html`
  467. })
  468. const {url, frameName} = await waitForEvent(webview, 'new-window')
  469. assert.equal(url, 'http://host/')
  470. assert.equal(frameName, 'host')
  471. })
  472. it('emits when link with target is called', async () => {
  473. loadWebView(webview, {
  474. src: `file://${fixtures}/pages/target-name.html`
  475. })
  476. const {url, frameName} = await waitForEvent(webview, 'new-window')
  477. assert.equal(url, 'http://host/')
  478. assert.equal(frameName, 'target')
  479. })
  480. })
  481. describe('ipc-message event', () => {
  482. it('emits when guest sends a ipc message to browser', async () => {
  483. loadWebView(webview, {
  484. nodeintegration: 'on',
  485. src: `file://${fixtures}/pages/ipc-message.html`
  486. })
  487. const {channel, args} = await waitForEvent(webview, 'ipc-message')
  488. assert.equal(channel, 'channel')
  489. assert.deepEqual(args, ['arg1', 'arg2'])
  490. })
  491. })
  492. describe('page-title-set event', () => {
  493. it('emits when title is set', async () => {
  494. loadWebView(webview, {
  495. src: `file://${fixtures}/pages/a.html`
  496. })
  497. const {title, explicitSet} = await waitForEvent(webview, 'page-title-set')
  498. assert.equal(title, 'test')
  499. assert(explicitSet)
  500. })
  501. })
  502. describe('page-favicon-updated event', () => {
  503. it('emits when favicon urls are received', async () => {
  504. loadWebView(webview, {
  505. src: `file://${fixtures}/pages/a.html`
  506. })
  507. const {favicons} = await waitForEvent(webview, 'page-favicon-updated')
  508. assert(favicons)
  509. assert.equal(favicons.length, 2)
  510. if (process.platform === 'win32') {
  511. assert(/^file:\/\/\/[A-Z]:\/favicon.png$/i.test(favicons[0]))
  512. } else {
  513. assert.equal(favicons[0], 'file:///favicon.png')
  514. }
  515. })
  516. })
  517. describe('will-navigate event', () => {
  518. it('emits when a url that leads to oustide of the page is clicked', async () => {
  519. loadWebView(webview, {
  520. src: `file://${fixtures}/pages/webview-will-navigate.html`
  521. })
  522. const {url} = await waitForEvent(webview, 'will-navigate')
  523. assert.equal(url, 'http://host/')
  524. })
  525. })
  526. describe('did-navigate event', () => {
  527. let p = path.join(fixtures, 'pages', 'webview-will-navigate.html')
  528. p = p.replace(/\\/g, '/')
  529. const pageUrl = url.format({
  530. protocol: 'file',
  531. slashes: true,
  532. pathname: p
  533. })
  534. it('emits when a url that leads to outside of the page is clicked', async () => {
  535. loadWebView(webview, {src: pageUrl})
  536. const {url} = await waitForEvent(webview, 'did-navigate')
  537. assert.equal(url, pageUrl)
  538. })
  539. })
  540. describe('did-navigate-in-page event', () => {
  541. it('emits when an anchor link is clicked', async () => {
  542. let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html')
  543. p = p.replace(/\\/g, '/')
  544. const pageUrl = url.format({
  545. protocol: 'file',
  546. slashes: true,
  547. pathname: p
  548. })
  549. loadWebView(webview, {src: pageUrl})
  550. const event = await waitForEvent(webview, 'did-navigate-in-page')
  551. assert.equal(event.url, `${pageUrl}#test_content`)
  552. })
  553. it('emits when window.history.replaceState is called', async () => {
  554. loadWebView(webview, {
  555. src: `file://${fixtures}/pages/webview-did-navigate-in-page-with-history.html`
  556. })
  557. const {url} = await waitForEvent(webview, 'did-navigate-in-page')
  558. assert.equal(url, 'http://host/')
  559. })
  560. it('emits when window.location.hash is changed', async () => {
  561. let p = path.join(fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html')
  562. p = p.replace(/\\/g, '/')
  563. const pageUrl = url.format({
  564. protocol: 'file',
  565. slashes: true,
  566. pathname: p
  567. })
  568. loadWebView(webview, {src: pageUrl})
  569. const event = await waitForEvent(webview, 'did-navigate-in-page')
  570. assert.equal(event.url, `${pageUrl}#test`)
  571. })
  572. })
  573. describe('close event', () => {
  574. it('should fire when interior page calls window.close', async () => {
  575. loadWebView(webview, {src: `file://${fixtures}/pages/close.html`})
  576. await waitForEvent(webview, 'close')
  577. })
  578. })
  579. describe('setDevToolsWebContents() API', () => {
  580. it('sets webContents of webview as devtools', async () => {
  581. const webview2 = new WebView()
  582. loadWebView(webview2)
  583. await waitForEvent(webview2, 'did-attach')
  584. // Setup an event handler for further usage.
  585. const waitForDomReady = waitForEvent(webview2, 'dom-ready')
  586. loadWebView(webview, {src: 'about:blank'})
  587. await waitForEvent(webview, 'dom-ready')
  588. webview.getWebContents().setDevToolsWebContents(webview2.getWebContents())
  589. webview.getWebContents().openDevTools()
  590. await waitForDomReady
  591. // Its WebContents should be a DevTools.
  592. const devtools = webview2.getWebContents()
  593. assert.ok(devtools.getURL().startsWith('chrome-devtools://devtools'))
  594. const name = await new Promise((resolve) => {
  595. devtools.executeJavaScript('InspectorFrontendHost.constructor.name', (name) => {
  596. resolve(name)
  597. })
  598. })
  599. document.body.removeChild(webview2)
  600. expect(name).to.be.equal('InspectorFrontendHostImpl')
  601. })
  602. })
  603. describe('devtools-opened event', () => {
  604. it('should fire when webview.openDevTools() is called', async () => {
  605. loadWebView(webview, {
  606. src: `file://${fixtures}/pages/base-page.html`
  607. })
  608. await waitForEvent(webview, 'dom-ready')
  609. webview.openDevTools()
  610. await waitForEvent(webview, 'devtools-opened')
  611. webview.closeDevTools()
  612. })
  613. })
  614. describe('devtools-closed event', () => {
  615. it('should fire when webview.closeDevTools() is called', async () => {
  616. loadWebView(webview, {
  617. src: `file://${fixtures}/pages/base-page.html`
  618. })
  619. await waitForEvent(webview, 'dom-ready')
  620. webview.openDevTools()
  621. await waitForEvent(webview, 'devtools-opened')
  622. webview.closeDevTools()
  623. await waitForEvent(webview, 'devtools-closed')
  624. })
  625. })
  626. describe('devtools-focused event', () => {
  627. it('should fire when webview.openDevTools() is called', async () => {
  628. loadWebView(webview, {
  629. src: `file://${fixtures}/pages/base-page.html`
  630. })
  631. const waitForDevToolsFocused = waitForEvent(webview, 'devtools-focused')
  632. await waitForEvent(webview, 'dom-ready')
  633. webview.openDevTools()
  634. await waitForDevToolsFocused
  635. webview.closeDevTools()
  636. })
  637. })
  638. describe('<webview>.reload()', () => {
  639. it('should emit beforeunload handler', async () => {
  640. await loadWebView(webview, {
  641. nodeintegration: 'on',
  642. src: `file://${fixtures}/pages/beforeunload-false.html`
  643. })
  644. // Event handler has to be added before reload.
  645. const waitForOnbeforeunload = waitForEvent(webview, 'ipc-message')
  646. webview.reload()
  647. const {channel} = await waitForOnbeforeunload
  648. assert.equal(channel, 'onbeforeunload')
  649. })
  650. })
  651. describe('<webview>.goForward()', () => {
  652. it('should work after a replaced history entry', (done) => {
  653. let loadCount = 1
  654. const listener = (e) => {
  655. if (loadCount === 1) {
  656. assert.equal(e.channel, 'history')
  657. assert.equal(e.args[0], 1)
  658. assert(!webview.canGoBack())
  659. assert(!webview.canGoForward())
  660. } else if (loadCount === 2) {
  661. assert.equal(e.channel, 'history')
  662. assert.equal(e.args[0], 2)
  663. assert(!webview.canGoBack())
  664. assert(webview.canGoForward())
  665. webview.removeEventListener('ipc-message', listener)
  666. }
  667. }
  668. const loadListener = () => {
  669. if (loadCount === 1) {
  670. webview.src = `file://${fixtures}/pages/base-page.html`
  671. } else if (loadCount === 2) {
  672. assert(webview.canGoBack())
  673. assert(!webview.canGoForward())
  674. webview.goBack()
  675. } else if (loadCount === 3) {
  676. webview.goForward()
  677. } else if (loadCount === 4) {
  678. assert(webview.canGoBack())
  679. assert(!webview.canGoForward())
  680. webview.removeEventListener('did-finish-load', loadListener)
  681. done()
  682. }
  683. loadCount += 1
  684. }
  685. webview.addEventListener('ipc-message', listener)
  686. webview.addEventListener('did-finish-load', loadListener)
  687. loadWebView(webview, {
  688. nodeintegration: 'on',
  689. src: `file://${fixtures}/pages/history-replace.html`
  690. })
  691. })
  692. })
  693. describe('<webview>.clearHistory()', () => {
  694. it('should clear the navigation history', async () => {
  695. loadWebView(webview, {
  696. nodeintegration: 'on',
  697. src: `file://${fixtures}/pages/history.html`
  698. })
  699. const event = await waitForEvent(webview, 'ipc-message')
  700. assert.equal(event.channel, 'history')
  701. assert.equal(event.args[0], 2)
  702. assert(webview.canGoBack())
  703. webview.clearHistory()
  704. assert(!webview.canGoBack())
  705. })
  706. })
  707. describe('basic auth', () => {
  708. const auth = require('basic-auth')
  709. it('should authenticate with correct credentials', (done) => {
  710. const message = 'Authenticated'
  711. const server = http.createServer((req, res) => {
  712. const credentials = auth(req)
  713. if (credentials.name === 'test' && credentials.pass === 'test') {
  714. res.end(message)
  715. } else {
  716. res.end('failed')
  717. }
  718. server.close()
  719. })
  720. server.listen(0, '127.0.0.1', () => {
  721. const port = server.address().port
  722. webview.addEventListener('ipc-message', (e) => {
  723. assert.equal(e.channel, message)
  724. done()
  725. })
  726. loadWebView(webview, {
  727. nodeintegration: 'on',
  728. src: `file://${fixtures}/pages/basic-auth.html?port=${port}`
  729. })
  730. })
  731. })
  732. })
  733. describe('dom-ready event', () => {
  734. it('emits when document is loaded', (done) => {
  735. const server = http.createServer(() => {})
  736. server.listen(0, '127.0.0.1', () => {
  737. const port = server.address().port
  738. webview.addEventListener('dom-ready', () => {
  739. done()
  740. })
  741. loadWebView(webview, {
  742. src: `file://${fixtures}/pages/dom-ready.html?port=${port}`
  743. })
  744. })
  745. })
  746. it('throws a custom error when an API method is called before the event is emitted', () => {
  747. const expectedErrorMessage =
  748. 'Cannot call stop because the webContents is unavailable. ' +
  749. 'The WebView must be attached to the DOM ' +
  750. 'and the dom-ready event emitted before this method can be called.'
  751. expect(() => { webview.stop() }).to.throw(expectedErrorMessage)
  752. })
  753. })
  754. describe('executeJavaScript', () => {
  755. it('should support user gesture', async () => {
  756. await loadWebView(webview, {
  757. src: `file://${fixtures}/pages/fullscreen.html`
  758. })
  759. // Event handler has to be added before js execution.
  760. const waitForEnterHtmlFullScreen = waitForEvent(webview, 'enter-html-full-screen')
  761. const jsScript = "document.querySelector('video').webkitRequestFullscreen()"
  762. webview.executeJavaScript(jsScript, true)
  763. return waitForEnterHtmlFullScreen
  764. })
  765. it('can return the result of the executed script', async () => {
  766. await loadWebView(webview, {
  767. src: 'about:blank'
  768. })
  769. const jsScript = "'4'+2"
  770. const expectedResult = '42'
  771. const result = await new Promise((resolve) => {
  772. webview.executeJavaScript(jsScript, false, (result) => {
  773. resolve(result)
  774. })
  775. })
  776. assert.equal(result, expectedResult)
  777. })
  778. })
  779. describe('sendInputEvent', () => {
  780. it('can send keyboard event', async () => {
  781. loadWebView(webview, {
  782. nodeintegration: 'on',
  783. src: `file://${fixtures}/pages/onkeyup.html`
  784. })
  785. await waitForEvent(webview, 'dom-ready')
  786. const waitForIpcMessage = waitForEvent(webview, 'ipc-message')
  787. webview.sendInputEvent({
  788. type: 'keyup',
  789. keyCode: 'c',
  790. modifiers: ['shift']
  791. })
  792. const {channel, args} = await waitForIpcMessage
  793. assert.equal(channel, 'keyup')
  794. assert.deepEqual(args, ['C', 'KeyC', 67, true, false])
  795. })
  796. it('can send mouse event', async () => {
  797. loadWebView(webview, {
  798. nodeintegration: 'on',
  799. src: `file://${fixtures}/pages/onmouseup.html`
  800. })
  801. await waitForEvent(webview, 'dom-ready')
  802. const waitForIpcMessage = waitForEvent(webview, 'ipc-message')
  803. webview.sendInputEvent({
  804. type: 'mouseup',
  805. modifiers: ['ctrl'],
  806. x: 10,
  807. y: 20
  808. })
  809. const {channel, args} = await waitForIpcMessage
  810. assert.equal(channel, 'mouseup')
  811. assert.deepEqual(args, [10, 20, false, true])
  812. })
  813. })
  814. describe('media-started-playing media-paused events', () => {
  815. it('emits when audio starts and stops playing', async () => {
  816. loadWebView(webview, {src: `file://${fixtures}/pages/audio.html`})
  817. await waitForEvent(webview, 'media-started-playing')
  818. await waitForEvent(webview, 'media-paused')
  819. })
  820. })
  821. describe('found-in-page event', () => {
  822. it('emits when a request is made', (done) => {
  823. let requestId = null
  824. let activeMatchOrdinal = []
  825. const listener = (e) => {
  826. assert.equal(e.result.requestId, requestId)
  827. assert.equal(e.result.matches, 3)
  828. activeMatchOrdinal.push(e.result.activeMatchOrdinal)
  829. if (e.result.activeMatchOrdinal === e.result.matches) {
  830. assert.deepEqual(activeMatchOrdinal, [1, 2, 3])
  831. webview.stopFindInPage('clearSelection')
  832. done()
  833. } else {
  834. listener2()
  835. }
  836. }
  837. const listener2 = () => {
  838. requestId = webview.findInPage('virtual')
  839. }
  840. webview.addEventListener('found-in-page', listener)
  841. webview.addEventListener('did-finish-load', listener2)
  842. loadWebView(webview, {src: `file://${fixtures}/pages/content.html`})
  843. // TODO(deepak1556): With https://codereview.chromium.org/2836973002
  844. // focus of the webContents is required when triggering the api.
  845. // Remove this workaround after determining the cause for
  846. // incorrect focus.
  847. webview.focus()
  848. })
  849. })
  850. describe('did-change-theme-color event', () => {
  851. it('emits when theme color changes', async () => {
  852. loadWebView(webview, {
  853. src: `file://${fixtures}/pages/theme-color.html`
  854. })
  855. await waitForEvent(webview, 'did-change-theme-color')
  856. })
  857. })
  858. describe('permission-request event', () => {
  859. function setUpRequestHandler (webview, requestedPermission, completed) {
  860. assert.ok(webview.partition)
  861. const listener = function (webContents, permission, callback) {
  862. if (webContents.getId() === webview.getId()) {
  863. // requestMIDIAccess with sysex requests both midi and midiSysex so
  864. // grant the first midi one and then reject the midiSysex one
  865. if (requestedPermission === 'midiSysex' && permission === 'midi') {
  866. return callback(true)
  867. }
  868. assert.equal(permission, requestedPermission)
  869. callback(false)
  870. if (completed) completed()
  871. }
  872. }
  873. session.fromPartition(webview.partition).setPermissionRequestHandler(listener)
  874. }
  875. it('emits when using navigator.getUserMedia api', (done) => {
  876. if (isCI) return done()
  877. webview.addEventListener('ipc-message', (e) => {
  878. assert.equal(e.channel, 'message')
  879. assert.deepEqual(e.args, ['PermissionDeniedError'])
  880. done()
  881. })
  882. webview.src = `file://${fixtures}/pages/permissions/media.html`
  883. webview.partition = 'permissionTest'
  884. webview.setAttribute('nodeintegration', 'on')
  885. setUpRequestHandler(webview, 'media')
  886. document.body.appendChild(webview)
  887. })
  888. it('emits when using navigator.geolocation api', (done) => {
  889. webview.addEventListener('ipc-message', (e) => {
  890. assert.equal(e.channel, 'message')
  891. assert.deepEqual(e.args, ['User denied Geolocation'])
  892. done()
  893. })
  894. webview.src = `file://${fixtures}/pages/permissions/geolocation.html`
  895. webview.partition = 'permissionTest'
  896. webview.setAttribute('nodeintegration', 'on')
  897. setUpRequestHandler(webview, 'geolocation')
  898. document.body.appendChild(webview)
  899. })
  900. it('emits when using navigator.requestMIDIAccess without sysex api', (done) => {
  901. webview.addEventListener('ipc-message', (e) => {
  902. assert.equal(e.channel, 'message')
  903. assert.deepEqual(e.args, ['SecurityError'])
  904. done()
  905. })
  906. webview.src = `file://${fixtures}/pages/permissions/midi.html`
  907. webview.partition = 'permissionTest'
  908. webview.setAttribute('nodeintegration', 'on')
  909. setUpRequestHandler(webview, 'midi')
  910. document.body.appendChild(webview)
  911. })
  912. it('emits when using navigator.requestMIDIAccess with sysex api', (done) => {
  913. webview.addEventListener('ipc-message', (e) => {
  914. assert.equal(e.channel, 'message')
  915. assert.deepEqual(e.args, ['SecurityError'])
  916. done()
  917. })
  918. webview.src = `file://${fixtures}/pages/permissions/midi-sysex.html`
  919. webview.partition = 'permissionTest'
  920. webview.setAttribute('nodeintegration', 'on')
  921. setUpRequestHandler(webview, 'midiSysex')
  922. document.body.appendChild(webview)
  923. })
  924. it('emits when accessing external protocol', (done) => {
  925. webview.src = 'magnet:test'
  926. webview.partition = 'permissionTest'
  927. setUpRequestHandler(webview, 'openExternal', done)
  928. document.body.appendChild(webview)
  929. })
  930. it('emits when using Notification.requestPermission', (done) => {
  931. webview.addEventListener('ipc-message', (e) => {
  932. assert.equal(e.channel, 'message')
  933. assert.deepEqual(e.args, ['granted'])
  934. done()
  935. })
  936. webview.src = `file://${fixtures}/pages/permissions/notification.html`
  937. webview.partition = 'permissionTest'
  938. webview.setAttribute('nodeintegration', 'on')
  939. session.fromPartition(webview.partition).setPermissionRequestHandler((webContents, permission, callback) => {
  940. if (webContents.getId() === webview.getId()) {
  941. assert.equal(permission, 'notifications')
  942. setTimeout(() => { callback(true) }, 10)
  943. }
  944. })
  945. document.body.appendChild(webview)
  946. })
  947. })
  948. describe('<webview>.getWebContents', () => {
  949. it('can return the webcontents associated', async () => {
  950. const src = 'about:blank'
  951. await loadWebView(webview, {src})
  952. const webviewContents = webview.getWebContents()
  953. assert(webviewContents)
  954. expect(webviewContents.getURL()).to.equal(src)
  955. })
  956. })
  957. describe('did-get-response-details event (deprecated)', () => {
  958. it('emits for the page and its resources', (done) => {
  959. // expected {fileName: resourceType} pairs
  960. const expectedResources = {
  961. 'did-get-response-details.html': 'mainFrame',
  962. 'logo.png': 'image'
  963. }
  964. let responses = 0
  965. webview.addEventListener('-did-get-response-details', (event) => {
  966. responses += 1
  967. const fileName = event.newURL.slice(event.newURL.lastIndexOf('/') + 1)
  968. const expectedType = expectedResources[fileName]
  969. assert(!!expectedType, `Unexpected response details for ${event.newURL}`)
  970. assert(typeof event.status === 'boolean', 'status should be boolean')
  971. assert.equal(event.httpResponseCode, 200)
  972. assert.equal(event.requestMethod, 'GET')
  973. assert(typeof event.referrer === 'string', 'referrer should be string')
  974. assert(!!event.headers, 'headers should be present')
  975. assert(typeof event.headers === 'object', 'headers should be object')
  976. assert.equal(event.resourceType, expectedType, 'Incorrect resourceType')
  977. if (responses === Object.keys(expectedResources).length) done()
  978. })
  979. webview.src = `file://${path.join(fixtures, 'pages', 'did-get-response-details.html')}`
  980. document.body.appendChild(webview)
  981. })
  982. })
  983. describe('document.visibilityState/hidden', () => {
  984. afterEach(() => {
  985. ipcMain.removeAllListeners('pong')
  986. })
  987. it('updates when the window is shown after the ready-to-show event', async () => {
  988. const w = await openTheWindow({ show: false })
  989. w.loadURL(`file://${fixtures}/pages/webview-visibilitychange.html`)
  990. await waitForOnce(w, 'ready-to-show')
  991. w.show()
  992. const [, visibilityState, hidden] = await waitForOnce(ipcMain, 'pong')
  993. assert(!hidden)
  994. assert.equal(visibilityState, 'visible')
  995. })
  996. it('inherits the parent window visibility state and receives visibilitychange events', async () => {
  997. const w = await openTheWindow({ show: false })
  998. w.loadURL(`file://${fixtures}/pages/webview-visibilitychange.html`)
  999. let [, visibilityState, hidden] = await waitForOnce(ipcMain, 'pong')
  1000. assert.equal(visibilityState, 'hidden')
  1001. assert.equal(hidden, true)
  1002. // We have to start waiting for the event
  1003. // before we ask the webContents to resize.
  1004. let getResponse = waitForOnce(ipcMain, 'pong')
  1005. w.webContents.emit('-window-visibility-change', 'visible')
  1006. return getResponse.then(([, visibilityState, hidden]) => {
  1007. assert.equal(visibilityState, 'visible')
  1008. assert.equal(hidden, false)
  1009. })
  1010. })
  1011. })
  1012. describe('will-attach-webview event', () => {
  1013. it('supports changing the web preferences', async () => {
  1014. ipcRenderer.send('disable-node-on-next-will-attach-webview')
  1015. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  1016. nodeintegration: 'yes',
  1017. src: `file://${fixtures}/pages/a.html`
  1018. })
  1019. const types = JSON.parse(message)
  1020. expect(types).to.include({
  1021. require: 'undefined',
  1022. module: 'undefined',
  1023. process: 'undefined',
  1024. global: 'undefined'
  1025. })
  1026. })
  1027. it('supports preventing a webview from being created', async () => {
  1028. ipcRenderer.send('prevent-next-will-attach-webview')
  1029. loadWebView(webview, {
  1030. src: `file://${fixtures}/pages/c.html`
  1031. })
  1032. await waitForEvent(webview, 'destroyed')
  1033. })
  1034. it('supports removing the preload script', async () => {
  1035. ipcRenderer.send('disable-preload-on-next-will-attach-webview')
  1036. const {message} = await startLoadingWebViewAndWaitForMessage(webview, {
  1037. nodeintegration: 'yes',
  1038. preload: path.join(fixtures, 'module', 'preload-set-global.js'),
  1039. src: `file://${fixtures}/pages/a.html`
  1040. })
  1041. assert.equal(message, 'undefined')
  1042. })
  1043. })
  1044. describe('did-attach-webview event', () => {
  1045. it('is emitted when a webview has been attached', async () => {
  1046. const w = await openTheWindow({ show: false })
  1047. w.loadURL(`file://${fixtures}/pages/webview-did-attach-event.html`)
  1048. const [, webContents] = await waitForOnce(w.webContents, 'did-attach-webview')
  1049. const [, id] = await waitForOnce(ipcMain, 'webview-dom-ready')
  1050. expect(webContents.id).to.equal(id)
  1051. })
  1052. })
  1053. it('loads devtools extensions registered on the parent window', async () => {
  1054. const w = await openTheWindow({ show: false })
  1055. BrowserWindow.removeDevToolsExtension('foo')
  1056. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo')
  1057. BrowserWindow.addDevToolsExtension(extensionPath)
  1058. w.loadURL(`file://${fixtures}/pages/webview-devtools.html`)
  1059. const [, {runtimeId, tabId}] = await waitForOnce(ipcMain, 'answer')
  1060. expect(runtimeId).to.equal('foo')
  1061. expect(tabId).to.be.not.equal(w.webContents.id)
  1062. })
  1063. describe('guestinstance attribute', () => {
  1064. it('before loading there is no attribute', () => {
  1065. loadWebView(webview) // Don't wait for loading to finish.
  1066. assert(!webview.hasAttribute('guestinstance'))
  1067. })
  1068. it('loading a page sets the guest view', async () => {
  1069. await loadWebView(webview, {
  1070. src: `file://${fixtures}/api/blank.html`
  1071. })
  1072. const instance = webview.getAttribute('guestinstance')
  1073. assert.equal(instance, parseInt(instance))
  1074. const guest = getGuestWebContents(parseInt(instance))
  1075. assert.equal(guest, webview.getWebContents())
  1076. })
  1077. it('deleting the attribute destroys the webview', async () => {
  1078. await loadWebView(webview, {
  1079. src: `file://${fixtures}/api/blank.html`
  1080. })
  1081. const instance = parseInt(webview.getAttribute('guestinstance'))
  1082. const waitForDestroy = waitForEvent(webview, 'destroyed')
  1083. webview.removeAttribute('guestinstance')
  1084. await waitForDestroy
  1085. expect(getGuestWebContents(instance)).to.equal(undefined)
  1086. })
  1087. it('setting the attribute on a new webview moves the contents', (done) => {
  1088. const loadListener = () => {
  1089. const webContents = webview.getWebContents()
  1090. const instance = webview.getAttribute('guestinstance')
  1091. const destroyListener = () => {
  1092. assert.equal(webContents, webview2.getWebContents())
  1093. // Make sure that events are hooked up to the right webview now
  1094. webview2.addEventListener('console-message', (e) => {
  1095. assert.equal(e.message, 'a')
  1096. document.body.removeChild(webview2)
  1097. done()
  1098. })
  1099. webview2.src = `file://${fixtures}/pages/a.html`
  1100. }
  1101. webview.addEventListener('destroyed', destroyListener, {once: true})
  1102. const webview2 = new WebView()
  1103. loadWebView(webview2, {
  1104. guestinstance: instance
  1105. })
  1106. }
  1107. webview.addEventListener('did-finish-load', loadListener, {once: true})
  1108. loadWebView(webview, {
  1109. src: `file://${fixtures}/api/blank.html`
  1110. })
  1111. })
  1112. it('setting the attribute to an invalid guestinstance does nothing', async () => {
  1113. await loadWebView(webview, {
  1114. src: `file://${fixtures}/api/blank.html`
  1115. })
  1116. webview.setAttribute('guestinstance', 55)
  1117. // Make sure that events are still hooked up to the webview
  1118. const waitForMessage = waitForEvent(webview, 'console-message')
  1119. webview.src = `file://${fixtures}/pages/a.html`
  1120. const {message} = await waitForMessage
  1121. assert.equal(message, 'a')
  1122. })
  1123. it('setting the attribute on an existing webview moves the contents', (done) => {
  1124. const load1Listener = () => {
  1125. const webContents = webview.getWebContents()
  1126. const instance = webview.getAttribute('guestinstance')
  1127. let destroyedInstance
  1128. const destroyListener = () => {
  1129. assert.equal(webContents, webview2.getWebContents())
  1130. assert.equal(null, getGuestWebContents(parseInt(destroyedInstance)))
  1131. // Make sure that events are hooked up to the right webview now
  1132. webview2.addEventListener('console-message', (e) => {
  1133. assert.equal(e.message, 'a')
  1134. document.body.removeChild(webview2)
  1135. done()
  1136. })
  1137. webview2.src = 'file://' + fixtures + '/pages/a.html'
  1138. }
  1139. webview.addEventListener('destroyed', destroyListener, {once: true})
  1140. const webview2 = new WebView()
  1141. const load2Listener = () => {
  1142. destroyedInstance = webview2.getAttribute('guestinstance')
  1143. assert.notEqual(instance, destroyedInstance)
  1144. webview2.setAttribute('guestinstance', instance)
  1145. }
  1146. webview2.addEventListener('did-finish-load', load2Listener, {once: true})
  1147. loadWebView(webview2, {
  1148. src: `file://${fixtures}/api/blank.html`
  1149. })
  1150. }
  1151. webview.addEventListener('did-finish-load', load1Listener, {once: true})
  1152. loadWebView(webview, {
  1153. src: `file://${fixtures}/api/blank.html`
  1154. })
  1155. })
  1156. it('moving a guest back to its original webview should work', (done) => {
  1157. loadWebView(webview, {
  1158. src: `file://${fixtures}/api/blank.html`
  1159. }).then(() => {
  1160. const webContents = webview.getWebContents()
  1161. const instance = webview.getAttribute('guestinstance')
  1162. const destroy1Listener = () => {
  1163. assert.equal(webContents, webview2.getWebContents())
  1164. assert.equal(null, webview.getWebContents())
  1165. const destroy2Listener = () => {
  1166. assert.equal(webContents, webview.getWebContents())
  1167. assert.equal(null, webview2.getWebContents())
  1168. // Make sure that events are hooked up to the right webview now
  1169. webview.addEventListener('console-message', (e) => {
  1170. document.body.removeChild(webview2)
  1171. assert.equal(e.message, 'a')
  1172. done()
  1173. })
  1174. webview.src = `file://${fixtures}/pages/a.html`
  1175. }
  1176. webview2.addEventListener('destroyed', destroy2Listener, {once: true})
  1177. webview.setAttribute('guestinstance', instance)
  1178. }
  1179. webview.addEventListener('destroyed', destroy1Listener, {once: true})
  1180. const webview2 = new WebView()
  1181. loadWebView(webview2, {guestinstance: instance})
  1182. })
  1183. })
  1184. // FIXME(alexeykuzmin): This test only passes if the previous test ^
  1185. // is run alongside.
  1186. it('setting the attribute on a webview in a different window moves the contents', (done) => {
  1187. loadWebView(webview, {
  1188. src: `file://${fixtures}/api/blank.html`
  1189. }).then(() => {
  1190. const instance = webview.getAttribute('guestinstance')
  1191. w = new BrowserWindow({ show: false })
  1192. w.webContents.once('did-finish-load', () => {
  1193. ipcMain.once('pong', () => {
  1194. assert(!webview.hasAttribute('guestinstance'))
  1195. done()
  1196. })
  1197. w.webContents.send('guestinstance', instance)
  1198. })
  1199. w.loadURL(`file://${fixtures}/pages/webview-move-to-window.html`)
  1200. })
  1201. })
  1202. it('does not delete the guestinstance attribute when moving the webview to another parent node', (done) => {
  1203. webview.addEventListener('dom-ready', function domReadyListener () {
  1204. webview.addEventListener('did-attach', () => {
  1205. assert(webview.guestinstance != null)
  1206. assert(webview.getWebContents() != null)
  1207. done()
  1208. })
  1209. document.body.replaceChild(webview, div)
  1210. })
  1211. webview.src = `file://${fixtures}/pages/a.html`
  1212. const div = document.createElement('div')
  1213. div.appendChild(webview)
  1214. document.body.appendChild(div)
  1215. })
  1216. it('does not destroy the webContents when hiding/showing the webview (regression)', (done) => {
  1217. webview.addEventListener('dom-ready', function () {
  1218. const instance = webview.getAttribute('guestinstance')
  1219. assert(instance != null)
  1220. // Wait for event directly since attach happens asynchronously over IPC
  1221. ipcMain.once('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', () => {
  1222. assert(webview.getWebContents() != null)
  1223. assert.equal(instance, webview.getAttribute('guestinstance'))
  1224. done()
  1225. })
  1226. webview.style.display = 'none'
  1227. webview.offsetHeight // eslint-disable-line
  1228. webview.style.display = 'block'
  1229. }, {once: true})
  1230. loadWebView(webview, {src: `file://${fixtures}/pages/a.html`})
  1231. })
  1232. it('does not reload the webContents when hiding/showing the webview (regression)', (done) => {
  1233. webview.addEventListener('dom-ready', function () {
  1234. webview.addEventListener('did-start-loading', () => {
  1235. done(new Error('webview started loading unexpectedly'))
  1236. })
  1237. // Wait for event directly since attach happens asynchronously over IPC
  1238. webview.addEventListener('did-attach', () => {
  1239. done()
  1240. })
  1241. webview.style.display = 'none'
  1242. webview.offsetHeight // eslint-disable-line
  1243. webview.style.display = 'block'
  1244. }, {once: true})
  1245. loadWebView(webview, {src: `file://${fixtures}/pages/a.html`})
  1246. })
  1247. })
  1248. describe('DOM events', () => {
  1249. let div
  1250. beforeEach(() => {
  1251. div = document.createElement('div')
  1252. div.style.width = '100px'
  1253. div.style.height = '10px'
  1254. div.style.overflow = 'hidden'
  1255. webview.style.height = '100%'
  1256. webview.style.width = '100%'
  1257. })
  1258. afterEach(() => {
  1259. if (div != null) div.remove()
  1260. })
  1261. it('emits resize events', (done) => {
  1262. webview.addEventListener('dom-ready', () => {
  1263. div.style.width = '1234px'
  1264. div.style.height = '789px'
  1265. })
  1266. webview.addEventListener('resize', function onResize (event) {
  1267. webview.removeEventListener('resize', onResize)
  1268. assert.equal(event.newWidth, 100)
  1269. assert.equal(event.newHeight, 10)
  1270. assert.equal(event.target, webview)
  1271. webview.addEventListener('resize', function onResizeAgain (event) {
  1272. // This will be triggered after setting the new div width and height.
  1273. webview.removeEventListener('resize', onResizeAgain)
  1274. assert.equal(event.newWidth, 1234)
  1275. assert.equal(event.newHeight, 789)
  1276. assert.equal(event.target, webview)
  1277. done()
  1278. })
  1279. })
  1280. webview.src = `file://${fixtures}/pages/a.html`
  1281. div.appendChild(webview)
  1282. document.body.appendChild(div)
  1283. })
  1284. })
  1285. describe('disableguestresize attribute', () => {
  1286. it('does not have attribute by default', () => {
  1287. loadWebView(webview)
  1288. assert(!webview.hasAttribute('disableguestresize'))
  1289. })
  1290. it('resizes guest when attribute is not present', async () => {
  1291. const INITIAL_SIZE = 200
  1292. const w = await openTheWindow(
  1293. {show: false, width: INITIAL_SIZE, height: INITIAL_SIZE})
  1294. w.loadURL(`file://${fixtures}/pages/webview-guest-resize.html`)
  1295. await waitForOnce(ipcMain, 'webview-loaded')
  1296. const elementResize = waitForOnce(ipcMain, 'webview-element-resize')
  1297. .then(([, width, height]) => {
  1298. assert.equal(width, CONTENT_SIZE)
  1299. assert.equal(height, CONTENT_SIZE)
  1300. })
  1301. const guestResize = waitForOnce(ipcMain, 'webview-guest-resize')
  1302. .then(([, width, height]) => {
  1303. assert.equal(width, CONTENT_SIZE)
  1304. assert.equal(height, CONTENT_SIZE)
  1305. })
  1306. const CONTENT_SIZE = 300
  1307. assert(CONTENT_SIZE !== INITIAL_SIZE)
  1308. w.setContentSize(CONTENT_SIZE, CONTENT_SIZE)
  1309. return Promise.all([elementResize, guestResize])
  1310. })
  1311. it('does not resize guest when attribute is present', async () => {
  1312. const INITIAL_SIZE = 200
  1313. const w = await openTheWindow(
  1314. {show: false, width: INITIAL_SIZE, height: INITIAL_SIZE})
  1315. w.loadURL(`file://${fixtures}/pages/webview-no-guest-resize.html`)
  1316. await waitForOnce(ipcMain, 'webview-loaded')
  1317. const noGuestResizePromise = Promise.race([
  1318. waitForOnce(ipcMain, 'webview-guest-resize'),
  1319. new Promise(resolve => setTimeout(() => resolve(), 500))
  1320. ]).then((eventData = null) => {
  1321. if (eventData !== null) {
  1322. // Means we got the 'webview-guest-resize' event before the time out.
  1323. return Promise.reject(new Error('Unexpected guest resize message'))
  1324. }
  1325. })
  1326. const CONTENT_SIZE = 300
  1327. assert(CONTENT_SIZE !== INITIAL_SIZE)
  1328. w.setContentSize(CONTENT_SIZE, CONTENT_SIZE)
  1329. return noGuestResizePromise
  1330. })
  1331. it('dispatches element resize event even when attribute is present', async () => {
  1332. const INITIAL_SIZE = 200
  1333. const w = await openTheWindow(
  1334. {show: false, width: INITIAL_SIZE, height: INITIAL_SIZE})
  1335. w.loadURL(`file://${fixtures}/pages/webview-no-guest-resize.html`)
  1336. await waitForOnce(ipcMain, 'webview-loaded')
  1337. const elementResizePromise = waitForOnce(ipcMain, 'webview-element-resize')
  1338. .then(([, width, height]) => {
  1339. expect(width).to.equal(CONTENT_SIZE)
  1340. expect(height).to.equal(CONTENT_SIZE)
  1341. })
  1342. const CONTENT_SIZE = 300
  1343. assert(CONTENT_SIZE !== INITIAL_SIZE)
  1344. w.setContentSize(CONTENT_SIZE, CONTENT_SIZE)
  1345. return elementResizePromise
  1346. })
  1347. it('can be manually resized with setSize even when attribute is present', async () => {
  1348. const INITIAL_SIZE = 200
  1349. const w = await openTheWindow(
  1350. {show: false, width: INITIAL_SIZE, height: INITIAL_SIZE})
  1351. w.loadURL(`file://${fixtures}/pages/webview-no-guest-resize.html`)
  1352. await waitForOnce(ipcMain, 'webview-loaded')
  1353. const GUEST_WIDTH = 10
  1354. const GUEST_HEIGHT = 20
  1355. const guestResizePromise = waitForOnce(ipcMain, 'webview-guest-resize')
  1356. .then(([, width, height]) => {
  1357. expect(width).to.be.equal(GUEST_WIDTH)
  1358. expect(height).to.be.equal(GUEST_HEIGHT)
  1359. })
  1360. const wc = webContents.getAllWebContents().find((wc) => {
  1361. return wc.hostWebContents &&
  1362. wc.hostWebContents.id === w.webContents.id
  1363. })
  1364. assert(wc)
  1365. wc.setSize({
  1366. normal: {
  1367. width: GUEST_WIDTH,
  1368. height: GUEST_HEIGHT
  1369. }
  1370. })
  1371. return guestResizePromise
  1372. })
  1373. })
  1374. describe('zoom behavior', () => {
  1375. const zoomScheme = remote.getGlobal('zoomScheme')
  1376. const webviewSession = session.fromPartition('webview-temp')
  1377. before((done) => {
  1378. const protocol = webviewSession.protocol
  1379. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  1380. callback('hello')
  1381. }, (error) => done(error))
  1382. })
  1383. after((done) => {
  1384. const protocol = webviewSession.protocol
  1385. protocol.unregisterProtocol(zoomScheme, (error) => done(error))
  1386. })
  1387. it('inherits the zoomFactor of the parent window', async () => {
  1388. const w = await openTheWindow({
  1389. show: false,
  1390. webPreferences: {
  1391. zoomFactor: 1.2
  1392. }
  1393. })
  1394. w.loadURL(`file://${fixtures}/pages/webview-zoom-factor.html`)
  1395. const [, zoomFactor, zoomLevel] = await waitForOnce(ipcMain, 'webview-parent-zoom-level')
  1396. expect(zoomFactor).to.equal(1.2)
  1397. expect(zoomLevel).to.equal(1)
  1398. })
  1399. it('maintains zoom level on navigation', async () => {
  1400. return openTheWindow({
  1401. show: false,
  1402. webPreferences: {
  1403. zoomFactor: 1.2
  1404. }
  1405. }).then((w) => {
  1406. const promise = new Promise((resolve) => {
  1407. ipcMain.on('webview-zoom-level', (event, zoomLevel, zoomFactor, newHost, final) => {
  1408. if (!newHost) {
  1409. expect(zoomFactor).to.equal(1.44)
  1410. expect(zoomLevel).to.equal(2.0)
  1411. } else {
  1412. expect(zoomFactor).to.equal(1.2)
  1413. expect(zoomLevel).to.equal(1)
  1414. }
  1415. if (final) {
  1416. resolve()
  1417. }
  1418. })
  1419. })
  1420. w.loadURL(`file://${fixtures}/pages/webview-custom-zoom-level.html`)
  1421. return promise
  1422. })
  1423. })
  1424. it('maintains zoom level when navigating within same page', async () => {
  1425. return openTheWindow({
  1426. show: false,
  1427. webPreferences: {
  1428. zoomFactor: 1.2
  1429. }
  1430. }).then((w) => {
  1431. const promise = new Promise((resolve) => {
  1432. ipcMain.on('webview-zoom-in-page', (event, zoomLevel, zoomFactor, final) => {
  1433. expect(zoomFactor).to.equal(1.44)
  1434. expect(zoomLevel).to.equal(2.0)
  1435. if (final) {
  1436. resolve()
  1437. }
  1438. })
  1439. })
  1440. w.loadURL(`file://${fixtures}/pages/webview-in-page-navigate.html`)
  1441. return promise
  1442. })
  1443. })
  1444. it('inherits zoom level for the origin when available', async () => {
  1445. const w = await openTheWindow({
  1446. show: false,
  1447. webPreferences: {
  1448. zoomFactor: 1.2
  1449. }
  1450. })
  1451. w.loadURL(`file://${fixtures}/pages/webview-origin-zoom-level.html`)
  1452. const [, zoomLevel] = await waitForOnce(ipcMain, 'webview-origin-zoom-level')
  1453. expect(zoomLevel).to.equal(2.0)
  1454. })
  1455. })
  1456. describe('nativeWindowOpen option', () => {
  1457. beforeEach(() => {
  1458. webview.setAttribute('allowpopups', 'on')
  1459. webview.setAttribute('nodeintegration', 'on')
  1460. webview.setAttribute('webpreferences', 'nativeWindowOpen=1')
  1461. })
  1462. it('opens window of about:blank with cross-scripting enabled', async () => {
  1463. // Don't wait for loading to finish.
  1464. loadWebView(webview, {
  1465. src: `file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}`
  1466. })
  1467. const [, content] = await waitForOnce(ipcMain, 'answer')
  1468. expect(content).to.equal('Hello')
  1469. })
  1470. it('opens window of same domain with cross-scripting enabled', async () => {
  1471. // Don't wait for loading to finish.
  1472. loadWebView(webview, {
  1473. src: `file://${path.join(fixtures, 'api', 'native-window-open-file.html')}`
  1474. })
  1475. const [, content] = await waitForOnce(ipcMain, 'answer')
  1476. expect(content).to.equal('Hello')
  1477. })
  1478. it('returns null from window.open when allowpopups is not set', async () => {
  1479. webview.removeAttribute('allowpopups')
  1480. // Don't wait for loading to finish.
  1481. loadWebView(webview, {
  1482. src: `file://${path.join(fixtures, 'api', 'native-window-open-no-allowpopups.html')}`
  1483. })
  1484. const [, {windowOpenReturnedNull}] = await waitForOnce(ipcMain, 'answer')
  1485. expect(windowOpenReturnedNull).to.equal(true)
  1486. })
  1487. it('blocks accessing cross-origin frames', async () => {
  1488. // Don't wait for loading to finish.
  1489. loadWebView(webview, {
  1490. src: `file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}`
  1491. })
  1492. const [, content] = await waitForOnce(ipcMain, 'answer')
  1493. const expectedContent =
  1494. 'Blocked a frame with origin "file://" from accessing a cross-origin frame.'
  1495. expect(content).to.equal(expectedContent)
  1496. })
  1497. it('emits a new-window event', async () => {
  1498. // Don't wait for loading to finish.
  1499. loadWebView(webview, {
  1500. src: `file://${fixtures}/pages/window-open.html`
  1501. })
  1502. const {url, frameName} = await waitForEvent(webview, 'new-window')
  1503. expect(url).to.equal('http://host/')
  1504. expect(frameName).to.equal('host')
  1505. })
  1506. it('emits a browser-window-created event', async () => {
  1507. // Don't wait for loading to finish.
  1508. loadWebView(webview, {
  1509. src: `file://${fixtures}/pages/window-open.html`
  1510. })
  1511. await waitForOnce(app, 'browser-window-created')
  1512. })
  1513. it('emits a web-contents-created event', (done) => {
  1514. app.on('web-contents-created', function listener (event, contents) {
  1515. if (contents.getType() === 'window') {
  1516. app.removeListener('web-contents-created', listener)
  1517. done()
  1518. }
  1519. })
  1520. loadWebView(webview, {
  1521. src: `file://${fixtures}/pages/window-open.html`
  1522. })
  1523. })
  1524. })
  1525. })