chromium-spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. const { expect } = require('chai')
  2. const fs = require('fs')
  3. const http = require('http')
  4. const path = require('path')
  5. const ws = require('ws')
  6. const url = require('url')
  7. const ChildProcess = require('child_process')
  8. const { ipcRenderer } = require('electron')
  9. const { emittedOnce } = require('./events-helpers')
  10. const { resolveGetters } = require('./expect-helpers')
  11. const features = process.electronBinding('features')
  12. /* Most of the APIs here don't use standard callbacks */
  13. /* eslint-disable standard/no-callback-literal */
  14. describe('chromium feature', () => {
  15. const fixtures = path.resolve(__dirname, 'fixtures')
  16. let listener = null
  17. afterEach(() => {
  18. if (listener != null) {
  19. window.removeEventListener('message', listener)
  20. }
  21. listener = null
  22. })
  23. describe('heap snapshot', () => {
  24. it('does not crash', function () {
  25. process.electronBinding('v8_util').takeHeapSnapshot()
  26. })
  27. })
  28. describe('navigator.webkitGetUserMedia', () => {
  29. it('calls its callbacks', (done) => {
  30. navigator.webkitGetUserMedia({
  31. audio: true,
  32. video: false
  33. }, () => done(),
  34. () => done())
  35. })
  36. })
  37. describe('navigator.language', () => {
  38. it('should not be empty', () => {
  39. expect(navigator.language).to.not.equal('')
  40. })
  41. })
  42. describe('navigator.geolocation', () => {
  43. before(function () {
  44. if (!features.isFakeLocationProviderEnabled()) {
  45. return this.skip()
  46. }
  47. })
  48. it('returns position when permission is granted', (done) => {
  49. navigator.geolocation.getCurrentPosition((position) => {
  50. expect(position).to.have.a.property('coords')
  51. expect(position).to.have.a.property('timestamp')
  52. done()
  53. }, (error) => {
  54. done(error)
  55. })
  56. })
  57. })
  58. describe('window.open', () => {
  59. it('returns a BrowserWindowProxy object', () => {
  60. const b = window.open('about:blank', '', 'show=no')
  61. expect(b.closed).to.be.false()
  62. expect(b.constructor.name).to.equal('BrowserWindowProxy')
  63. b.close()
  64. })
  65. it('accepts "nodeIntegration" as feature', (done) => {
  66. let b = null
  67. listener = (event) => {
  68. expect(event.data.isProcessGlobalUndefined).to.be.true()
  69. b.close()
  70. done()
  71. }
  72. window.addEventListener('message', listener)
  73. b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no')
  74. })
  75. it('inherit options of parent window', (done) => {
  76. let b = null
  77. listener = (event) => {
  78. const width = outerWidth
  79. const height = outerHeight
  80. expect(event.data).to.equal(`size: ${width} ${height}`)
  81. b.close()
  82. done()
  83. }
  84. window.addEventListener('message', listener)
  85. b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no')
  86. })
  87. it('disables node integration when it is disabled on the parent window', (done) => {
  88. let b = null
  89. listener = (event) => {
  90. expect(event.data.isProcessGlobalUndefined).to.be.true()
  91. b.close()
  92. done()
  93. }
  94. window.addEventListener('message', listener)
  95. const windowUrl = require('url').format({
  96. pathname: `${fixtures}/pages/window-opener-no-node-integration.html`,
  97. protocol: 'file',
  98. query: {
  99. p: `${fixtures}/pages/window-opener-node.html`
  100. },
  101. slashes: true
  102. })
  103. b = window.open(windowUrl, '', 'nodeIntegration=no,show=no')
  104. })
  105. it('disables the <webview> tag when it is disabled on the parent window', (done) => {
  106. let b = null
  107. listener = (event) => {
  108. expect(event.data.isWebViewGlobalUndefined).to.be.true()
  109. b.close()
  110. done()
  111. }
  112. window.addEventListener('message', listener)
  113. const windowUrl = require('url').format({
  114. pathname: `${fixtures}/pages/window-opener-no-webview-tag.html`,
  115. protocol: 'file',
  116. query: {
  117. p: `${fixtures}/pages/window-opener-webview.html`
  118. },
  119. slashes: true
  120. })
  121. b = window.open(windowUrl, '', 'webviewTag=no,nodeIntegration=yes,show=no')
  122. })
  123. it('does not override child options', (done) => {
  124. let b = null
  125. const size = {
  126. width: 350,
  127. height: 450
  128. }
  129. listener = (event) => {
  130. expect(event.data).to.equal(`size: ${size.width} ${size.height}`)
  131. b.close()
  132. done()
  133. }
  134. window.addEventListener('message', listener)
  135. b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height)
  136. })
  137. it('throws an exception when the arguments cannot be converted to strings', () => {
  138. expect(() => {
  139. window.open('', { toString: null })
  140. }).to.throw('Cannot convert object to primitive value')
  141. expect(() => {
  142. window.open('', '', { toString: 3 })
  143. }).to.throw('Cannot convert object to primitive value')
  144. })
  145. it('does not throw an exception when the features include webPreferences', () => {
  146. let b = null
  147. expect(() => {
  148. b = window.open('', '', 'webPreferences=')
  149. }).to.not.throw()
  150. b.close()
  151. })
  152. })
  153. describe('window.opener', () => {
  154. it('is not null for window opened by window.open', (done) => {
  155. let b = null
  156. listener = (event) => {
  157. expect(event.data).to.equal('object')
  158. b.close()
  159. done()
  160. }
  161. window.addEventListener('message', listener)
  162. b = window.open(`file://${fixtures}/pages/window-opener.html`, '', 'show=no')
  163. })
  164. })
  165. describe('window.postMessage', () => {
  166. it('throws an exception when the targetOrigin cannot be converted to a string', () => {
  167. const b = window.open('')
  168. expect(() => {
  169. b.postMessage('test', { toString: null })
  170. }).to.throw('Cannot convert object to primitive value')
  171. b.close()
  172. })
  173. })
  174. describe('window.opener.postMessage', () => {
  175. it('sets source and origin correctly', (done) => {
  176. let b = null
  177. listener = (event) => {
  178. window.removeEventListener('message', listener)
  179. b.close()
  180. expect(event.source).to.equal(b)
  181. expect(event.origin).to.equal('file://')
  182. done()
  183. }
  184. window.addEventListener('message', listener)
  185. b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no')
  186. })
  187. it('supports windows opened from a <webview>', (done) => {
  188. const webview = new WebView()
  189. webview.addEventListener('console-message', (e) => {
  190. webview.remove()
  191. expect(e.message).to.equal('message')
  192. done()
  193. })
  194. webview.allowpopups = true
  195. webview.src = url.format({
  196. pathname: `${fixtures}/pages/webview-opener-postMessage.html`,
  197. protocol: 'file',
  198. query: {
  199. p: `${fixtures}/pages/window-opener-postMessage.html`
  200. },
  201. slashes: true
  202. })
  203. document.body.appendChild(webview)
  204. })
  205. describe('targetOrigin argument', () => {
  206. let serverURL
  207. let server
  208. beforeEach((done) => {
  209. server = http.createServer((req, res) => {
  210. res.writeHead(200)
  211. const filePath = path.join(fixtures, 'pages', 'window-opener-targetOrigin.html')
  212. res.end(fs.readFileSync(filePath, 'utf8'))
  213. })
  214. server.listen(0, '127.0.0.1', () => {
  215. serverURL = `http://127.0.0.1:${server.address().port}`
  216. done()
  217. })
  218. })
  219. afterEach(() => {
  220. server.close()
  221. })
  222. it('delivers messages that match the origin', (done) => {
  223. let b = null
  224. listener = (event) => {
  225. window.removeEventListener('message', listener)
  226. b.close()
  227. expect(event.data).to.equal('deliver')
  228. done()
  229. }
  230. window.addEventListener('message', listener)
  231. b = window.open(serverURL, '', 'show=no')
  232. })
  233. })
  234. })
  235. describe('webgl', () => {
  236. before(function () {
  237. if (process.platform === 'win32') {
  238. this.skip()
  239. }
  240. })
  241. it('can be get as context in canvas', () => {
  242. if (process.platform === 'linux') {
  243. // FIXME(alexeykuzmin): Skip the test.
  244. // this.skip()
  245. return
  246. }
  247. const webgl = document.createElement('canvas').getContext('webgl')
  248. expect(webgl).to.not.be.null()
  249. })
  250. })
  251. describe('web workers', () => {
  252. it('Worker can work', (done) => {
  253. const worker = new Worker('../fixtures/workers/worker.js')
  254. const message = 'ping'
  255. worker.onmessage = (event) => {
  256. expect(event.data).to.equal(message)
  257. worker.terminate()
  258. done()
  259. }
  260. worker.postMessage(message)
  261. })
  262. it('Worker has no node integration by default', (done) => {
  263. const worker = new Worker('../fixtures/workers/worker_node.js')
  264. worker.onmessage = (event) => {
  265. expect(event.data).to.equal('undefined undefined undefined undefined')
  266. worker.terminate()
  267. done()
  268. }
  269. })
  270. it('Worker has node integration with nodeIntegrationInWorker', (done) => {
  271. const webview = new WebView()
  272. webview.addEventListener('ipc-message', (e) => {
  273. expect(e.channel).to.equal('object function object function')
  274. webview.remove()
  275. done()
  276. })
  277. webview.src = `file://${fixtures}/pages/worker.html`
  278. webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker')
  279. document.body.appendChild(webview)
  280. })
  281. // FIXME: disabled during chromium update due to crash in content::WorkerScriptFetchInitiator::CreateScriptLoaderOnIO
  282. xdescribe('SharedWorker', () => {
  283. it('can work', (done) => {
  284. const worker = new SharedWorker('../fixtures/workers/shared_worker.js')
  285. const message = 'ping'
  286. worker.port.onmessage = (event) => {
  287. expect(event.data).to.equal(message)
  288. done()
  289. }
  290. worker.port.postMessage(message)
  291. })
  292. it('has no node integration by default', (done) => {
  293. const worker = new SharedWorker('../fixtures/workers/shared_worker_node.js')
  294. worker.port.onmessage = (event) => {
  295. expect(event.data).to.equal('undefined undefined undefined undefined')
  296. done()
  297. }
  298. })
  299. it('has node integration with nodeIntegrationInWorker', (done) => {
  300. const webview = new WebView()
  301. webview.addEventListener('console-message', (e) => {
  302. console.log(e)
  303. })
  304. webview.addEventListener('ipc-message', (e) => {
  305. expect(e.channel).to.equal('object function object function')
  306. webview.remove()
  307. done()
  308. })
  309. webview.src = `file://${fixtures}/pages/shared_worker.html`
  310. webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker')
  311. document.body.appendChild(webview)
  312. })
  313. })
  314. })
  315. describe('iframe', () => {
  316. let iframe = null
  317. beforeEach(() => {
  318. iframe = document.createElement('iframe')
  319. })
  320. afterEach(() => {
  321. document.body.removeChild(iframe)
  322. })
  323. it('does not have node integration', (done) => {
  324. iframe.src = `file://${fixtures}/pages/set-global.html`
  325. document.body.appendChild(iframe)
  326. iframe.onload = () => {
  327. expect(iframe.contentWindow.test).to.equal('undefined undefined undefined')
  328. done()
  329. }
  330. })
  331. })
  332. describe('storage', () => {
  333. describe('DOM storage quota increase', () => {
  334. ['localStorage', 'sessionStorage'].forEach((storageName) => {
  335. const storage = window[storageName]
  336. it(`allows saving at least 40MiB in ${storageName}`, (done) => {
  337. // Although JavaScript strings use UTF-16, the underlying
  338. // storage provider may encode strings differently, muddling the
  339. // translation between character and byte counts. However,
  340. // a string of 40 * 2^20 characters will require at least 40MiB
  341. // and presumably no more than 80MiB, a size guaranteed to
  342. // to exceed the original 10MiB quota yet stay within the
  343. // new 100MiB quota.
  344. // Note that both the key name and value affect the total size.
  345. const testKeyName = '_electronDOMStorageQuotaIncreasedTest'
  346. const length = 40 * Math.pow(2, 20) - testKeyName.length
  347. storage.setItem(testKeyName, 'X'.repeat(length))
  348. // Wait at least one turn of the event loop to help avoid false positives
  349. // Although not entirely necessary, the previous version of this test case
  350. // failed to detect a real problem (perhaps related to DOM storage data caching)
  351. // wherein calling `getItem` immediately after `setItem` would appear to work
  352. // but then later (e.g. next tick) it would not.
  353. setTimeout(() => {
  354. expect(storage.getItem(testKeyName)).to.have.lengthOf(length)
  355. storage.removeItem(testKeyName)
  356. done()
  357. }, 1)
  358. })
  359. it(`throws when attempting to use more than 128MiB in ${storageName}`, () => {
  360. expect(() => {
  361. const testKeyName = '_electronDOMStorageQuotaStillEnforcedTest'
  362. const length = 128 * Math.pow(2, 20) - testKeyName.length
  363. try {
  364. storage.setItem(testKeyName, 'X'.repeat(length))
  365. } finally {
  366. storage.removeItem(testKeyName)
  367. }
  368. }).to.throw()
  369. })
  370. })
  371. })
  372. it('requesting persitent quota works', (done) => {
  373. navigator.webkitPersistentStorage.requestQuota(1024 * 1024, (grantedBytes) => {
  374. expect(grantedBytes).to.equal(1048576)
  375. done()
  376. })
  377. })
  378. })
  379. describe('websockets', () => {
  380. let wss = null
  381. let server = null
  382. const WebSocketServer = ws.Server
  383. afterEach(() => {
  384. wss.close()
  385. server.close()
  386. })
  387. it('has user agent', (done) => {
  388. server = http.createServer()
  389. server.listen(0, '127.0.0.1', () => {
  390. const port = server.address().port
  391. wss = new WebSocketServer({ server: server })
  392. wss.on('error', done)
  393. wss.on('connection', (ws, upgradeReq) => {
  394. if (upgradeReq.headers['user-agent']) {
  395. done()
  396. } else {
  397. done('user agent is empty')
  398. }
  399. })
  400. const socket = new WebSocket(`ws://127.0.0.1:${port}`)
  401. })
  402. })
  403. })
  404. describe('Promise', () => {
  405. it('resolves correctly in Node.js calls', (done) => {
  406. class XElement extends HTMLElement {}
  407. customElements.define('x-element', XElement)
  408. setImmediate(() => {
  409. let called = false
  410. Promise.resolve().then(() => {
  411. done(called ? void 0 : new Error('wrong sequence'))
  412. })
  413. document.createElement('x-element')
  414. called = true
  415. })
  416. })
  417. it('resolves correctly in Electron calls', (done) => {
  418. class YElement extends HTMLElement {}
  419. customElements.define('y-element', YElement)
  420. ipcRenderer.invoke('ping').then(() => {
  421. let called = false
  422. Promise.resolve().then(() => {
  423. done(called ? void 0 : new Error('wrong sequence'))
  424. })
  425. document.createElement('y-element')
  426. called = true
  427. })
  428. })
  429. })
  430. describe('fetch', () => {
  431. it('does not crash', (done) => {
  432. const server = http.createServer((req, res) => {
  433. res.end('test')
  434. server.close()
  435. })
  436. server.listen(0, '127.0.0.1', () => {
  437. const port = server.address().port
  438. fetch(`http://127.0.0.1:${port}`).then((res) => res.body.getReader())
  439. .then((reader) => {
  440. reader.read().then((r) => {
  441. reader.cancel()
  442. done()
  443. })
  444. }).catch((e) => done(e))
  445. })
  446. })
  447. })
  448. describe('window.alert(message, title)', () => {
  449. it('throws an exception when the arguments cannot be converted to strings', () => {
  450. expect(() => {
  451. window.alert({ toString: null })
  452. }).to.throw('Cannot convert object to primitive value')
  453. })
  454. })
  455. describe('window.confirm(message, title)', () => {
  456. it('throws an exception when the arguments cannot be converted to strings', () => {
  457. expect(() => {
  458. window.confirm({ toString: null }, 'title')
  459. }).to.throw('Cannot convert object to primitive value')
  460. })
  461. })
  462. describe('window.history', () => {
  463. describe('window.history.go(offset)', () => {
  464. it('throws an exception when the argumnet cannot be converted to a string', () => {
  465. expect(() => {
  466. window.history.go({ toString: null })
  467. }).to.throw('Cannot convert object to primitive value')
  468. })
  469. })
  470. })
  471. // TODO(nornagon): this is broken on CI, it triggers:
  472. // [FATAL:speech_synthesis.mojom-shared.h(237)] The outgoing message will
  473. // trigger VALIDATION_ERROR_UNEXPECTED_NULL_POINTER at the receiving side
  474. // (null text in SpeechSynthesisUtterance struct).
  475. describe.skip('SpeechSynthesis', () => {
  476. before(function () {
  477. if (!features.isTtsEnabled()) {
  478. this.skip()
  479. }
  480. })
  481. it('should emit lifecycle events', async () => {
  482. const sentence = `long sentence which will take at least a few seconds to
  483. utter so that it's possible to pause and resume before the end`
  484. const utter = new SpeechSynthesisUtterance(sentence)
  485. // Create a dummy utterence so that speech synthesis state
  486. // is initialized for later calls.
  487. speechSynthesis.speak(new SpeechSynthesisUtterance())
  488. speechSynthesis.cancel()
  489. speechSynthesis.speak(utter)
  490. // paused state after speak()
  491. expect(speechSynthesis.paused).to.be.false()
  492. await new Promise((resolve) => { utter.onstart = resolve })
  493. // paused state after start event
  494. expect(speechSynthesis.paused).to.be.false()
  495. speechSynthesis.pause()
  496. // paused state changes async, right before the pause event
  497. expect(speechSynthesis.paused).to.be.false()
  498. await new Promise((resolve) => { utter.onpause = resolve })
  499. expect(speechSynthesis.paused).to.be.true()
  500. speechSynthesis.resume()
  501. await new Promise((resolve) => { utter.onresume = resolve })
  502. // paused state after resume event
  503. expect(speechSynthesis.paused).to.be.false()
  504. await new Promise((resolve) => { utter.onend = resolve })
  505. })
  506. })
  507. })
  508. describe('console functions', () => {
  509. it('should exist', () => {
  510. expect(console.log, 'log').to.be.a('function')
  511. expect(console.error, 'error').to.be.a('function')
  512. expect(console.warn, 'warn').to.be.a('function')
  513. expect(console.info, 'info').to.be.a('function')
  514. expect(console.debug, 'debug').to.be.a('function')
  515. expect(console.trace, 'trace').to.be.a('function')
  516. expect(console.time, 'time').to.be.a('function')
  517. expect(console.timeEnd, 'timeEnd').to.be.a('function')
  518. })
  519. })