chromium-spec.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352
  1. const assert = require('assert')
  2. const chai = require('chai')
  3. const dirtyChai = require('dirty-chai')
  4. const fs = require('fs')
  5. const http = require('http')
  6. const path = require('path')
  7. const ws = require('ws')
  8. const url = require('url')
  9. const ChildProcess = require('child_process')
  10. const { ipcRenderer, remote } = require('electron')
  11. const { closeWindow } = require('./window-helpers')
  12. const { resolveGetters } = require('./assert-helpers')
  13. const { app, BrowserWindow, ipcMain, protocol, session, webContents } = remote
  14. const isCI = remote.getGlobal('isCi')
  15. const features = process.atomBinding('features')
  16. const { expect } = chai
  17. chai.use(dirtyChai)
  18. /* Most of the APIs here don't use standard callbacks */
  19. /* eslint-disable standard/no-callback-literal */
  20. describe('chromium feature', () => {
  21. const fixtures = path.resolve(__dirname, 'fixtures')
  22. let listener = null
  23. let w = null
  24. afterEach(() => {
  25. if (listener != null) {
  26. window.removeEventListener('message', listener)
  27. }
  28. listener = null
  29. })
  30. describe('command line switches', () => {
  31. describe('--lang switch', () => {
  32. const currentLocale = app.getLocale()
  33. const testLocale = (locale, result, done) => {
  34. const appPath = path.join(__dirname, 'fixtures', 'api', 'locale-check')
  35. const electronPath = remote.getGlobal('process').execPath
  36. let output = ''
  37. const appProcess = ChildProcess.spawn(electronPath, [appPath, `--lang=${locale}`])
  38. appProcess.stdout.on('data', (data) => { output += data })
  39. appProcess.stdout.on('end', () => {
  40. output = output.replace(/(\r\n|\n|\r)/gm, '')
  41. assert.strictEqual(output, result)
  42. done()
  43. })
  44. }
  45. it('should set the locale', (done) => testLocale('fr', 'fr', done))
  46. it('should not set an invalid locale', (done) => testLocale('asdfkl', currentLocale, done))
  47. })
  48. describe('--remote-debugging-port switch', () => {
  49. it('should display the discovery page', (done) => {
  50. const electronPath = remote.getGlobal('process').execPath
  51. let output = ''
  52. const appProcess = ChildProcess.spawn(electronPath, [`--remote-debugging-port=`])
  53. appProcess.stderr.on('data', (data) => {
  54. output += data
  55. const m = /DevTools listening on ws:\/\/127.0.0.1:(\d+)\//.exec(output)
  56. if (m) {
  57. appProcess.stderr.removeAllListeners('data')
  58. const port = m[1]
  59. http.get(`http://127.0.0.1:${port}`, (res) => {
  60. res.destroy()
  61. appProcess.kill()
  62. expect(res.statusCode).to.eql(200)
  63. expect(parseInt(res.headers['content-length'])).to.be.greaterThan(0)
  64. done()
  65. })
  66. }
  67. })
  68. })
  69. })
  70. })
  71. afterEach(() => closeWindow(w).then(() => { w = null }))
  72. describe('heap snapshot', () => {
  73. it('does not crash', function () {
  74. process.atomBinding('v8_util').takeHeapSnapshot()
  75. })
  76. })
  77. describe('sending request of http protocol urls', () => {
  78. it('does not crash', (done) => {
  79. const server = http.createServer((req, res) => {
  80. res.end()
  81. server.close()
  82. done()
  83. })
  84. server.listen(0, '127.0.0.1', () => {
  85. const port = server.address().port
  86. $.get(`http://127.0.0.1:${port}`)
  87. })
  88. })
  89. })
  90. describe('accessing key names also used as Node.js module names', () => {
  91. it('does not crash', (done) => {
  92. w = new BrowserWindow({ show: false })
  93. w.webContents.once('did-finish-load', () => { done() })
  94. w.webContents.once('crashed', () => done(new Error('WebContents crashed.')))
  95. w.loadFile(path.join(fixtures, 'pages', 'external-string.html'))
  96. })
  97. })
  98. describe('loading jquery', () => {
  99. it('does not crash', (done) => {
  100. w = new BrowserWindow({
  101. show: false,
  102. webPreferences: {
  103. nodeIntegration: false
  104. }
  105. })
  106. w.webContents.once('did-finish-load', () => { done() })
  107. w.webContents.once('crashed', () => done(new Error('WebContents crashed.')))
  108. w.loadFile(path.join(fixtures, 'pages', 'jquery.html'))
  109. })
  110. })
  111. describe('navigator.webkitGetUserMedia', () => {
  112. it('calls its callbacks', (done) => {
  113. navigator.webkitGetUserMedia({
  114. audio: true,
  115. video: false
  116. }, () => done(),
  117. () => done())
  118. })
  119. })
  120. describe('navigator.mediaDevices', () => {
  121. if (isCI) return
  122. afterEach(() => {
  123. remote.getGlobal('permissionChecks').allow()
  124. })
  125. it('can return labels of enumerated devices', (done) => {
  126. navigator.mediaDevices.enumerateDevices().then((devices) => {
  127. const labels = devices.map((device) => device.label)
  128. const labelFound = labels.some((label) => !!label)
  129. if (labelFound) {
  130. done()
  131. } else {
  132. done(new Error(`No device labels found: ${JSON.stringify(labels)}`))
  133. }
  134. }).catch(done)
  135. })
  136. it('does not return labels of enumerated devices when permission denied', (done) => {
  137. remote.getGlobal('permissionChecks').reject()
  138. navigator.mediaDevices.enumerateDevices().then((devices) => {
  139. const labels = devices.map((device) => device.label)
  140. const labelFound = labels.some((label) => !!label)
  141. if (labelFound) {
  142. done(new Error(`Device labels were found: ${JSON.stringify(labels)}`))
  143. } else {
  144. done()
  145. }
  146. }).catch(done)
  147. })
  148. it('can return new device id when cookie storage is cleared', (done) => {
  149. const options = {
  150. origin: null,
  151. storages: ['cookies']
  152. }
  153. const deviceIds = []
  154. const ses = session.fromPartition('persist:media-device-id')
  155. w = new BrowserWindow({
  156. show: false,
  157. webPreferences: {
  158. session: ses
  159. }
  160. })
  161. w.webContents.on('ipc-message', (event, args) => {
  162. if (args[0] === 'deviceIds') deviceIds.push(args[1])
  163. if (deviceIds.length === 2) {
  164. assert.notDeepStrictEqual(deviceIds[0], deviceIds[1])
  165. closeWindow(w).then(() => {
  166. w = null
  167. done()
  168. }).catch((error) => done(error))
  169. } else {
  170. ses.clearStorageData(options, () => {
  171. w.webContents.reload()
  172. })
  173. }
  174. })
  175. w.loadFile(path.join(fixtures, 'pages', 'media-id-reset.html'))
  176. })
  177. })
  178. describe('navigator.language', () => {
  179. it('should not be empty', () => {
  180. assert.notStrictEqual(navigator.language, '')
  181. })
  182. })
  183. describe('navigator.languages', (done) => {
  184. it('should return the system locale only', () => {
  185. const appLocale = app.getLocale()
  186. assert.strictEqual(navigator.languages.length, 1)
  187. assert.strictEqual(navigator.languages[0], appLocale)
  188. })
  189. })
  190. describe('navigator.serviceWorker', () => {
  191. it('should register for file scheme', (done) => {
  192. w = new BrowserWindow({
  193. show: false,
  194. webPreferences: {
  195. partition: 'sw-file-scheme-spec'
  196. }
  197. })
  198. w.webContents.on('ipc-message', (event, args) => {
  199. if (args[0] === 'reload') {
  200. w.webContents.reload()
  201. } else if (args[0] === 'error') {
  202. done(args[1])
  203. } else if (args[0] === 'response') {
  204. assert.strictEqual(args[1], 'Hello from serviceWorker!')
  205. session.fromPartition('sw-file-scheme-spec').clearStorageData({
  206. storages: ['serviceworkers']
  207. }, () => done())
  208. }
  209. })
  210. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')))
  211. w.loadFile(path.join(fixtures, 'pages', 'service-worker', 'index.html'))
  212. })
  213. it('should register for intercepted file scheme', (done) => {
  214. const customSession = session.fromPartition('intercept-file')
  215. customSession.protocol.interceptBufferProtocol('file', (request, callback) => {
  216. let file = url.parse(request.url).pathname
  217. if (file[0] === '/' && process.platform === 'win32') file = file.slice(1)
  218. const content = fs.readFileSync(path.normalize(file))
  219. const ext = path.extname(file)
  220. let type = 'text/html'
  221. if (ext === '.js') type = 'application/javascript'
  222. callback({ data: content, mimeType: type })
  223. }, (error) => {
  224. if (error) done(error)
  225. })
  226. w = new BrowserWindow({
  227. show: false,
  228. webPreferences: { session: customSession }
  229. })
  230. w.webContents.on('ipc-message', (event, args) => {
  231. if (args[0] === 'reload') {
  232. w.webContents.reload()
  233. } else if (args[0] === 'error') {
  234. done(`unexpected error : ${args[1]}`)
  235. } else if (args[0] === 'response') {
  236. assert.strictEqual(args[1], 'Hello from serviceWorker!')
  237. customSession.clearStorageData({
  238. storages: ['serviceworkers']
  239. }, () => {
  240. customSession.protocol.uninterceptProtocol('file', (error) => done(error))
  241. })
  242. }
  243. })
  244. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')))
  245. w.loadFile(path.join(fixtures, 'pages', 'service-worker', 'index.html'))
  246. })
  247. })
  248. describe('navigator.geolocation', () => {
  249. before(function () {
  250. if (!features.isFakeLocationProviderEnabled()) {
  251. return this.skip()
  252. }
  253. })
  254. it('returns position when permission is granted', (done) => {
  255. navigator.geolocation.getCurrentPosition((position) => {
  256. assert(position.timestamp)
  257. done()
  258. }, (error) => {
  259. done(error)
  260. })
  261. })
  262. it('returns error when permission is denied', (done) => {
  263. w = new BrowserWindow({
  264. show: false,
  265. webPreferences: {
  266. partition: 'geolocation-spec'
  267. }
  268. })
  269. w.webContents.on('ipc-message', (event, args) => {
  270. if (args[0] === 'success') {
  271. done()
  272. } else {
  273. done('unexpected response from geolocation api')
  274. }
  275. })
  276. w.webContents.session.setPermissionRequestHandler((wc, permission, callback) => {
  277. if (permission === 'geolocation') {
  278. callback(false)
  279. } else {
  280. callback(true)
  281. }
  282. })
  283. w.loadFile(path.join(fixtures, 'pages', 'geolocation', 'index.html'))
  284. })
  285. })
  286. describe('window.open', () => {
  287. it('returns a BrowserWindowProxy object', () => {
  288. const b = window.open('about:blank', '', 'show=no')
  289. assert.strictEqual(b.closed, false)
  290. assert.strictEqual(b.constructor.name, 'BrowserWindowProxy')
  291. b.close()
  292. })
  293. it('accepts "nodeIntegration" as feature', (done) => {
  294. let b = null
  295. listener = (event) => {
  296. assert.strictEqual(event.data.isProcessGlobalUndefined, true)
  297. b.close()
  298. done()
  299. }
  300. window.addEventListener('message', listener)
  301. b = window.open(`file://${fixtures}/pages/window-opener-node.html`, '', 'nodeIntegration=no,show=no')
  302. })
  303. it('inherit options of parent window', (done) => {
  304. let b = null
  305. listener = (event) => {
  306. const ref1 = remote.getCurrentWindow().getSize()
  307. const width = ref1[0]
  308. const height = ref1[1]
  309. assert.strictEqual(event.data, `size: ${width} ${height}`)
  310. b.close()
  311. done()
  312. }
  313. window.addEventListener('message', listener)
  314. b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no')
  315. })
  316. for (const show of [true, false]) {
  317. it(`inherits parent visibility over parent {show=${show}} option`, (done) => {
  318. const w = new BrowserWindow({ show })
  319. // toggle visibility
  320. if (show) {
  321. w.hide()
  322. } else {
  323. w.show()
  324. }
  325. w.webContents.once('new-window', (e, url, frameName, disposition, options) => {
  326. assert.strictEqual(options.show, w.isVisible())
  327. w.close()
  328. done()
  329. })
  330. w.loadFile(path.join(fixtures, 'pages', 'window-open.html'))
  331. })
  332. }
  333. it('disables node integration when it is disabled on the parent window', (done) => {
  334. let b = null
  335. listener = (event) => {
  336. assert.strictEqual(event.data.isProcessGlobalUndefined, true)
  337. b.close()
  338. done()
  339. }
  340. window.addEventListener('message', listener)
  341. const windowUrl = require('url').format({
  342. pathname: `${fixtures}/pages/window-opener-no-node-integration.html`,
  343. protocol: 'file',
  344. query: {
  345. p: `${fixtures}/pages/window-opener-node.html`
  346. },
  347. slashes: true
  348. })
  349. b = window.open(windowUrl, '', 'nodeIntegration=no,show=no')
  350. })
  351. it('disables webviewTag when node integration is disabled on the parent window', (done) => {
  352. let b = null
  353. listener = (event) => {
  354. assert.strictEqual(event.data.isWebViewUndefined, true)
  355. b.close()
  356. done()
  357. }
  358. window.addEventListener('message', listener)
  359. const windowUrl = require('url').format({
  360. pathname: `${fixtures}/pages/window-opener-no-web-view-tag.html`,
  361. protocol: 'file',
  362. query: {
  363. p: `${fixtures}/pages/window-opener-web-view.html`
  364. },
  365. slashes: true
  366. })
  367. b = window.open(windowUrl, '', 'nodeIntegration=no,show=no')
  368. })
  369. // TODO(codebytere): re-enable this test
  370. xit('disables node integration when it is disabled on the parent window for chrome devtools URLs', (done) => {
  371. let b = null
  372. app.once('web-contents-created', (event, contents) => {
  373. contents.once('did-finish-load', () => {
  374. contents.executeJavaScript('typeof process').then((typeofProcessGlobal) => {
  375. assert.strictEqual(typeofProcessGlobal, 'undefined')
  376. b.close()
  377. done()
  378. }).catch(done)
  379. })
  380. })
  381. b = window.open('chrome-devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no')
  382. })
  383. it('disables JavaScript when it is disabled on the parent window', (done) => {
  384. let b = null
  385. app.once('web-contents-created', (event, contents) => {
  386. contents.once('did-finish-load', () => {
  387. app.once('browser-window-created', (event, window) => {
  388. const preferences = window.webContents.getLastWebPreferences()
  389. assert.strictEqual(preferences.javascript, false)
  390. window.destroy()
  391. b.close()
  392. done()
  393. })
  394. // Click link on page
  395. contents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: 1, y: 1 })
  396. contents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: 1, y: 1 })
  397. })
  398. })
  399. const windowUrl = require('url').format({
  400. pathname: `${fixtures}/pages/window-no-javascript.html`,
  401. protocol: 'file',
  402. slashes: true
  403. })
  404. b = window.open(windowUrl, '', 'javascript=no,show=no')
  405. })
  406. it('disables the <webview> tag when it is disabled on the parent window', (done) => {
  407. let b = null
  408. listener = (event) => {
  409. assert.strictEqual(event.data.isWebViewGlobalUndefined, true)
  410. b.close()
  411. done()
  412. }
  413. window.addEventListener('message', listener)
  414. const windowUrl = require('url').format({
  415. pathname: `${fixtures}/pages/window-opener-no-webview-tag.html`,
  416. protocol: 'file',
  417. query: {
  418. p: `${fixtures}/pages/window-opener-webview.html`
  419. },
  420. slashes: true
  421. })
  422. b = window.open(windowUrl, '', 'webviewTag=no,nodeIntegration=yes,show=no')
  423. })
  424. it('does not override child options', (done) => {
  425. let b = null
  426. const size = {
  427. width: 350,
  428. height: 450
  429. }
  430. listener = (event) => {
  431. assert.strictEqual(event.data, `size: ${size.width} ${size.height}`)
  432. b.close()
  433. done()
  434. }
  435. window.addEventListener('message', listener)
  436. b = window.open(`file://${fixtures}/pages/window-open-size.html`, '', 'show=no,width=' + size.width + ',height=' + size.height)
  437. })
  438. it('handles cycles when merging the parent options into the child options', (done) => {
  439. w = BrowserWindow.fromId(ipcRenderer.sendSync('create-window-with-options-cycle'))
  440. w.loadFile(path.join(fixtures, 'pages', 'window-open.html'))
  441. w.webContents.once('new-window', (event, url, frameName, disposition, options) => {
  442. assert.strictEqual(options.show, false)
  443. assert.deepStrictEqual(...resolveGetters(options.foo, {
  444. bar: undefined,
  445. baz: {
  446. hello: {
  447. world: true
  448. }
  449. },
  450. baz2: {
  451. hello: {
  452. world: true
  453. }
  454. }
  455. }))
  456. done()
  457. })
  458. })
  459. it('defines a window.location getter', (done) => {
  460. let b = null
  461. let targetURL
  462. if (process.platform === 'win32') {
  463. targetURL = `file:///${fixtures.replace(/\\/g, '/')}/pages/base-page.html`
  464. } else {
  465. targetURL = `file://${fixtures}/pages/base-page.html`
  466. }
  467. app.once('browser-window-created', (event, window) => {
  468. window.webContents.once('did-finish-load', () => {
  469. assert.strictEqual(b.location, targetURL)
  470. b.close()
  471. done()
  472. })
  473. })
  474. b = window.open(targetURL)
  475. })
  476. it('defines a window.location setter', (done) => {
  477. let b = null
  478. app.once('browser-window-created', (event, { webContents }) => {
  479. webContents.once('did-finish-load', () => {
  480. // When it loads, redirect
  481. b.location = `file://${fixtures}/pages/base-page.html`
  482. webContents.once('did-finish-load', () => {
  483. // After our second redirect, cleanup and callback
  484. b.close()
  485. done()
  486. })
  487. })
  488. })
  489. b = window.open('about:blank')
  490. })
  491. it('open a blank page when no URL is specified', (done) => {
  492. let b = null
  493. app.once('browser-window-created', (event, { webContents }) => {
  494. webContents.once('did-finish-load', () => {
  495. const { location } = b
  496. b.close()
  497. assert.strictEqual(location, 'about:blank')
  498. let c = null
  499. app.once('browser-window-created', (event, { webContents }) => {
  500. webContents.once('did-finish-load', () => {
  501. const { location } = c
  502. c.close()
  503. assert.strictEqual(location, 'about:blank')
  504. done()
  505. })
  506. })
  507. c = window.open('')
  508. })
  509. })
  510. b = window.open()
  511. })
  512. it('throws an exception when the arguments cannot be converted to strings', () => {
  513. assert.throws(() => {
  514. window.open('', { toString: null })
  515. }, /Cannot convert object to primitive value/)
  516. assert.throws(() => {
  517. window.open('', '', { toString: 3 })
  518. }, /Cannot convert object to primitive value/)
  519. })
  520. it('sets the window title to the specified frameName', (done) => {
  521. let b = null
  522. app.once('browser-window-created', (event, createdWindow) => {
  523. assert.strictEqual(createdWindow.getTitle(), 'hello')
  524. b.close()
  525. done()
  526. })
  527. b = window.open('', 'hello')
  528. })
  529. it('does not throw an exception when the frameName is a built-in object property', (done) => {
  530. let b = null
  531. app.once('browser-window-created', (event, createdWindow) => {
  532. assert.strictEqual(createdWindow.getTitle(), '__proto__')
  533. b.close()
  534. done()
  535. })
  536. b = window.open('', '__proto__')
  537. })
  538. it('does not throw an exception when the features include webPreferences', () => {
  539. let b = null
  540. assert.doesNotThrow(() => {
  541. b = window.open('', '', 'webPreferences=')
  542. })
  543. b.close()
  544. })
  545. })
  546. describe('window.opener', () => {
  547. const url = `file://${fixtures}/pages/window-opener.html`
  548. it('is null for main window', (done) => {
  549. w = new BrowserWindow({ show: false })
  550. w.webContents.once('ipc-message', (event, args) => {
  551. assert.deepStrictEqual(args, ['opener', null])
  552. done()
  553. })
  554. w.loadFile(path.join(fixtures, 'pages', 'window-opener.html'))
  555. })
  556. it('is not null for window opened by window.open', (done) => {
  557. let b = null
  558. listener = (event) => {
  559. assert.strictEqual(event.data, 'object')
  560. b.close()
  561. done()
  562. }
  563. window.addEventListener('message', listener)
  564. b = window.open(url, '', 'show=no')
  565. })
  566. })
  567. describe('window.opener access from BrowserWindow', () => {
  568. const scheme = 'other'
  569. const url = `${scheme}://${fixtures}/pages/window-opener-location.html`
  570. let w = null
  571. before((done) => {
  572. protocol.registerFileProtocol(scheme, (request, callback) => {
  573. callback(`${fixtures}/pages/window-opener-location.html`)
  574. }, (error) => done(error))
  575. })
  576. after(() => {
  577. protocol.unregisterProtocol(scheme)
  578. })
  579. afterEach(() => {
  580. w.close()
  581. })
  582. it('does nothing when origin of current window does not match opener', (done) => {
  583. listener = (event) => {
  584. assert.strictEqual(event.data, null)
  585. done()
  586. }
  587. window.addEventListener('message', listener)
  588. w = window.open(url, '', 'show=no,nodeIntegration=no')
  589. })
  590. it('works when origin matches', (done) => {
  591. listener = (event) => {
  592. assert.strictEqual(event.data, location.href)
  593. done()
  594. }
  595. window.addEventListener('message', listener)
  596. w = window.open(`file://${fixtures}/pages/window-opener-location.html`, '', 'show=no,nodeIntegration=no')
  597. })
  598. it('works when origin does not match opener but has node integration', (done) => {
  599. listener = (event) => {
  600. assert.strictEqual(event.data, location.href)
  601. done()
  602. }
  603. window.addEventListener('message', listener)
  604. w = window.open(url, '', 'show=no,nodeIntegration=yes')
  605. })
  606. })
  607. describe('window.opener access from <webview>', () => {
  608. const scheme = 'other'
  609. const srcPath = `${fixtures}/pages/webview-opener-postMessage.html`
  610. const pageURL = `file://${fixtures}/pages/window-opener-location.html`
  611. let webview = null
  612. before((done) => {
  613. protocol.registerFileProtocol(scheme, (request, callback) => {
  614. callback(srcPath)
  615. }, (error) => done(error))
  616. })
  617. after(() => {
  618. protocol.unregisterProtocol(scheme)
  619. })
  620. afterEach(() => {
  621. if (webview != null) webview.remove()
  622. })
  623. it('does nothing when origin of webview src URL does not match opener', (done) => {
  624. webview = new WebView()
  625. webview.addEventListener('console-message', (e) => {
  626. assert.strictEqual(e.message, 'null')
  627. done()
  628. })
  629. webview.setAttribute('allowpopups', 'on')
  630. webview.src = url.format({
  631. pathname: srcPath,
  632. protocol: scheme,
  633. query: {
  634. p: pageURL
  635. },
  636. slashes: true
  637. })
  638. document.body.appendChild(webview)
  639. })
  640. it('works when origin matches', (done) => {
  641. webview = new WebView()
  642. webview.addEventListener('console-message', (e) => {
  643. assert.strictEqual(e.message, webview.src)
  644. done()
  645. })
  646. webview.setAttribute('allowpopups', 'on')
  647. webview.src = url.format({
  648. pathname: srcPath,
  649. protocol: 'file',
  650. query: {
  651. p: pageURL
  652. },
  653. slashes: true
  654. })
  655. document.body.appendChild(webview)
  656. })
  657. it('works when origin does not match opener but has node integration', (done) => {
  658. webview = new WebView()
  659. webview.addEventListener('console-message', (e) => {
  660. webview.remove()
  661. assert.strictEqual(e.message, webview.src)
  662. done()
  663. })
  664. webview.setAttribute('allowpopups', 'on')
  665. webview.setAttribute('nodeintegration', 'on')
  666. webview.src = url.format({
  667. pathname: srcPath,
  668. protocol: scheme,
  669. query: {
  670. p: pageURL
  671. },
  672. slashes: true
  673. })
  674. document.body.appendChild(webview)
  675. })
  676. })
  677. describe('window.postMessage', () => {
  678. it('sets the source and origin correctly', (done) => {
  679. let b = null
  680. listener = (event) => {
  681. window.removeEventListener('message', listener)
  682. b.close()
  683. const message = JSON.parse(event.data)
  684. assert.strictEqual(message.data, 'testing')
  685. assert.strictEqual(message.origin, 'file://')
  686. assert.strictEqual(message.sourceEqualsOpener, true)
  687. assert.strictEqual(event.origin, 'file://')
  688. done()
  689. }
  690. window.addEventListener('message', listener)
  691. app.once('browser-window-created', (event, { webContents }) => {
  692. webContents.once('did-finish-load', () => {
  693. b.postMessage('testing', '*')
  694. })
  695. })
  696. b = window.open(`file://${fixtures}/pages/window-open-postMessage.html`, '', 'show=no')
  697. })
  698. it('throws an exception when the targetOrigin cannot be converted to a string', () => {
  699. const b = window.open('')
  700. assert.throws(() => {
  701. b.postMessage('test', { toString: null })
  702. }, /Cannot convert object to primitive value/)
  703. b.close()
  704. })
  705. })
  706. describe('window.opener.postMessage', () => {
  707. it('sets source and origin correctly', (done) => {
  708. let b = null
  709. listener = (event) => {
  710. window.removeEventListener('message', listener)
  711. b.close()
  712. assert.strictEqual(event.source, b)
  713. assert.strictEqual(event.origin, 'file://')
  714. done()
  715. }
  716. window.addEventListener('message', listener)
  717. b = window.open(`file://${fixtures}/pages/window-opener-postMessage.html`, '', 'show=no')
  718. })
  719. it('supports windows opened from a <webview>', (done) => {
  720. const webview = new WebView()
  721. webview.addEventListener('console-message', (e) => {
  722. webview.remove()
  723. assert.strictEqual(e.message, 'message')
  724. done()
  725. })
  726. webview.allowpopups = true
  727. webview.src = url.format({
  728. pathname: `${fixtures}/pages/webview-opener-postMessage.html`,
  729. protocol: 'file',
  730. query: {
  731. p: `${fixtures}/pages/window-opener-postMessage.html`
  732. },
  733. slashes: true
  734. })
  735. document.body.appendChild(webview)
  736. })
  737. describe('targetOrigin argument', () => {
  738. let serverURL
  739. let server
  740. beforeEach((done) => {
  741. server = http.createServer((req, res) => {
  742. res.writeHead(200)
  743. const filePath = path.join(fixtures, 'pages', 'window-opener-targetOrigin.html')
  744. res.end(fs.readFileSync(filePath, 'utf8'))
  745. })
  746. server.listen(0, '127.0.0.1', () => {
  747. serverURL = `http://127.0.0.1:${server.address().port}`
  748. done()
  749. })
  750. })
  751. afterEach(() => {
  752. server.close()
  753. })
  754. it('delivers messages that match the origin', (done) => {
  755. let b = null
  756. listener = (event) => {
  757. window.removeEventListener('message', listener)
  758. b.close()
  759. assert.strictEqual(event.data, 'deliver')
  760. done()
  761. }
  762. window.addEventListener('message', listener)
  763. b = window.open(serverURL, '', 'show=no')
  764. })
  765. })
  766. })
  767. describe('creating a Uint8Array under browser side', () => {
  768. it('does not crash', () => {
  769. const RUint8Array = remote.getGlobal('Uint8Array')
  770. const arr = new RUint8Array()
  771. assert(arr)
  772. })
  773. })
  774. describe('webgl', () => {
  775. before(function () {
  776. if (isCI && process.platform === 'win32') {
  777. this.skip()
  778. }
  779. })
  780. it('can be get as context in canvas', () => {
  781. if (process.platform === 'linux') {
  782. // FIXME(alexeykuzmin): Skip the test.
  783. // this.skip()
  784. return
  785. }
  786. const webgl = document.createElement('canvas').getContext('webgl')
  787. assert.notStrictEqual(webgl, null)
  788. })
  789. })
  790. describe('web workers', () => {
  791. it('Worker can work', (done) => {
  792. const worker = new Worker('../fixtures/workers/worker.js')
  793. const message = 'ping'
  794. worker.onmessage = (event) => {
  795. assert.strictEqual(event.data, message)
  796. worker.terminate()
  797. done()
  798. }
  799. worker.postMessage(message)
  800. })
  801. it('Worker has no node integration by default', (done) => {
  802. const worker = new Worker('../fixtures/workers/worker_node.js')
  803. worker.onmessage = (event) => {
  804. assert.strictEqual(event.data, 'undefined undefined undefined undefined')
  805. worker.terminate()
  806. done()
  807. }
  808. })
  809. it('Worker has node integration with nodeIntegrationInWorker', (done) => {
  810. const webview = new WebView()
  811. webview.addEventListener('ipc-message', (e) => {
  812. assert.strictEqual(e.channel, 'object function object function')
  813. webview.remove()
  814. done()
  815. })
  816. webview.src = `file://${fixtures}/pages/worker.html`
  817. webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker')
  818. document.body.appendChild(webview)
  819. })
  820. it('SharedWorker can work', (done) => {
  821. const worker = new SharedWorker('../fixtures/workers/shared_worker.js')
  822. const message = 'ping'
  823. worker.port.onmessage = (event) => {
  824. assert.strictEqual(event.data, message)
  825. done()
  826. }
  827. worker.port.postMessage(message)
  828. })
  829. it('SharedWorker has no node integration by default', (done) => {
  830. const worker = new SharedWorker('../fixtures/workers/shared_worker_node.js')
  831. worker.port.onmessage = (event) => {
  832. assert.strictEqual(event.data, 'undefined undefined undefined undefined')
  833. done()
  834. }
  835. })
  836. it('SharedWorker has node integration with nodeIntegrationInWorker', (done) => {
  837. const webview = new WebView()
  838. webview.addEventListener('console-message', (e) => {
  839. console.log(e)
  840. })
  841. webview.addEventListener('ipc-message', (e) => {
  842. assert.strictEqual(e.channel, 'object function object function')
  843. webview.remove()
  844. done()
  845. })
  846. webview.src = `file://${fixtures}/pages/shared_worker.html`
  847. webview.setAttribute('webpreferences', 'nodeIntegration, nodeIntegrationInWorker')
  848. document.body.appendChild(webview)
  849. })
  850. })
  851. describe('iframe', () => {
  852. let iframe = null
  853. beforeEach(() => {
  854. iframe = document.createElement('iframe')
  855. })
  856. afterEach(() => {
  857. document.body.removeChild(iframe)
  858. })
  859. it('does not have node integration', (done) => {
  860. iframe.src = `file://${fixtures}/pages/set-global.html`
  861. document.body.appendChild(iframe)
  862. iframe.onload = () => {
  863. assert.strictEqual(iframe.contentWindow.test, 'undefined undefined undefined')
  864. done()
  865. }
  866. })
  867. })
  868. describe('storage', () => {
  869. describe('DOM storage quota override', () => {
  870. ['localStorage', 'sessionStorage'].forEach((storageName) => {
  871. it(`allows saving at least 50MiB in ${storageName}`, () => {
  872. const storage = window[storageName]
  873. const testKeyName = '_electronDOMStorageQuotaOverrideTest'
  874. // 25 * 2^20 UTF-16 characters will require 50MiB
  875. const arraySize = 25 * Math.pow(2, 20)
  876. storage[testKeyName] = new Array(arraySize).fill('X').join('')
  877. expect(storage[testKeyName]).to.have.lengthOf(arraySize)
  878. delete storage[testKeyName]
  879. })
  880. })
  881. })
  882. it('requesting persitent quota works', (done) => {
  883. navigator.webkitPersistentStorage.requestQuota(1024 * 1024, (grantedBytes) => {
  884. assert.strictEqual(grantedBytes, 1048576)
  885. done()
  886. })
  887. })
  888. describe('custom non standard schemes', () => {
  889. const protocolName = 'storage'
  890. let contents = null
  891. before((done) => {
  892. const handler = (request, callback) => {
  893. const parsedUrl = url.parse(request.url)
  894. let filename
  895. switch (parsedUrl.pathname) {
  896. case '/localStorage' : filename = 'local_storage.html'; break
  897. case '/sessionStorage' : filename = 'session_storage.html'; break
  898. case '/WebSQL' : filename = 'web_sql.html'; break
  899. case '/indexedDB' : filename = 'indexed_db.html'; break
  900. case '/cookie' : filename = 'cookie.html'; break
  901. default : filename = ''
  902. }
  903. callback({ path: `${fixtures}/pages/storage/${filename}` })
  904. }
  905. protocol.registerFileProtocol(protocolName, handler, (error) => done(error))
  906. })
  907. after((done) => {
  908. protocol.unregisterProtocol(protocolName, () => done())
  909. })
  910. beforeEach(() => {
  911. contents = webContents.create({})
  912. })
  913. afterEach(() => {
  914. contents.destroy()
  915. contents = null
  916. })
  917. it('cannot access localStorage', (done) => {
  918. ipcMain.once('local-storage-response', (event, error) => {
  919. assert.strictEqual(
  920. error,
  921. 'Failed to read the \'localStorage\' property from \'Window\': Access is denied for this document.')
  922. done()
  923. })
  924. contents.loadURL(protocolName + '://host/localStorage')
  925. })
  926. it('cannot access sessionStorage', (done) => {
  927. ipcMain.once('session-storage-response', (event, error) => {
  928. assert.strictEqual(
  929. error,
  930. 'Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.')
  931. done()
  932. })
  933. contents.loadURL(`${protocolName}://host/sessionStorage`)
  934. })
  935. it('cannot access WebSQL database', (done) => {
  936. ipcMain.once('web-sql-response', (event, error) => {
  937. assert.strictEqual(
  938. error,
  939. 'An attempt was made to break through the security policy of the user agent.')
  940. done()
  941. })
  942. contents.loadURL(`${protocolName}://host/WebSQL`)
  943. })
  944. it('cannot access indexedDB', (done) => {
  945. ipcMain.once('indexed-db-response', (event, error) => {
  946. assert.strictEqual(error, 'The user denied permission to access the database.')
  947. done()
  948. })
  949. contents.loadURL(`${protocolName}://host/indexedDB`)
  950. })
  951. it('cannot access cookie', (done) => {
  952. ipcMain.once('cookie-response', (event, cookie) => {
  953. assert(!cookie)
  954. done()
  955. })
  956. contents.loadURL(`${protocolName}://host/cookie`)
  957. })
  958. })
  959. })
  960. describe('websockets', () => {
  961. let wss = null
  962. let server = null
  963. const WebSocketServer = ws.Server
  964. afterEach(() => {
  965. wss.close()
  966. server.close()
  967. })
  968. it('has user agent', (done) => {
  969. server = http.createServer()
  970. server.listen(0, '127.0.0.1', () => {
  971. const port = server.address().port
  972. wss = new WebSocketServer({ server: server })
  973. wss.on('error', done)
  974. wss.on('connection', (ws, upgradeReq) => {
  975. if (upgradeReq.headers['user-agent']) {
  976. done()
  977. } else {
  978. done('user agent is empty')
  979. }
  980. })
  981. const socket = new WebSocket(`ws://127.0.0.1:${port}`)
  982. assert(socket)
  983. })
  984. })
  985. })
  986. describe('Promise', () => {
  987. it('resolves correctly in Node.js calls', (done) => {
  988. document.registerElement('x-element', {
  989. prototype: Object.create(HTMLElement.prototype, {
  990. createdCallback: {
  991. value: () => {}
  992. }
  993. })
  994. })
  995. setImmediate(() => {
  996. let called = false
  997. Promise.resolve().then(() => {
  998. done(called ? void 0 : new Error('wrong sequence'))
  999. })
  1000. document.createElement('x-element')
  1001. called = true
  1002. })
  1003. })
  1004. it('resolves correctly in Electron calls', (done) => {
  1005. document.registerElement('y-element', {
  1006. prototype: Object.create(HTMLElement.prototype, {
  1007. createdCallback: {
  1008. value: () => {}
  1009. }
  1010. })
  1011. })
  1012. remote.getGlobal('setImmediate')(() => {
  1013. let called = false
  1014. Promise.resolve().then(() => {
  1015. done(called ? void 0 : new Error('wrong sequence'))
  1016. })
  1017. document.createElement('y-element')
  1018. called = true
  1019. })
  1020. })
  1021. })
  1022. describe('fetch', () => {
  1023. it('does not crash', (done) => {
  1024. const server = http.createServer((req, res) => {
  1025. res.end('test')
  1026. server.close()
  1027. })
  1028. server.listen(0, '127.0.0.1', () => {
  1029. const port = server.address().port
  1030. fetch(`http://127.0.0.1:${port}`).then((res) => res.body.getReader())
  1031. .then((reader) => {
  1032. reader.read().then((r) => {
  1033. reader.cancel()
  1034. done()
  1035. })
  1036. }).catch((e) => done(e))
  1037. })
  1038. })
  1039. })
  1040. describe('PDF Viewer', () => {
  1041. before(function () {
  1042. if (!features.isPDFViewerEnabled()) {
  1043. return this.skip()
  1044. }
  1045. })
  1046. beforeEach(() => {
  1047. this.pdfSource = url.format({
  1048. pathname: path.join(fixtures, 'assets', 'cat.pdf').replace(/\\/g, '/'),
  1049. protocol: 'file',
  1050. slashes: true
  1051. })
  1052. this.pdfSourceWithParams = url.format({
  1053. pathname: path.join(fixtures, 'assets', 'cat.pdf').replace(/\\/g, '/'),
  1054. query: {
  1055. a: 1,
  1056. b: 2
  1057. },
  1058. protocol: 'file',
  1059. slashes: true
  1060. })
  1061. this.createBrowserWindow = ({ plugins, preload }) => {
  1062. w = new BrowserWindow({
  1063. show: false,
  1064. webPreferences: {
  1065. preload: path.join(fixtures, 'module', preload),
  1066. plugins: plugins
  1067. }
  1068. })
  1069. }
  1070. this.testPDFIsLoadedInSubFrame = (page, preloadFile, done) => {
  1071. const pagePath = url.format({
  1072. pathname: path.join(fixtures, 'pages', page).replace(/\\/g, '/'),
  1073. protocol: 'file',
  1074. slashes: true
  1075. })
  1076. this.createBrowserWindow({ plugins: true, preload: preloadFile })
  1077. ipcMain.once('pdf-loaded', (event, state) => {
  1078. assert.strictEqual(state, 'success')
  1079. done()
  1080. })
  1081. w.webContents.on('page-title-updated', () => {
  1082. const parsedURL = url.parse(w.webContents.getURL(), true)
  1083. assert.strictEqual(parsedURL.protocol, 'chrome:')
  1084. assert.strictEqual(parsedURL.hostname, 'pdf-viewer')
  1085. assert.strictEqual(parsedURL.query.src, pagePath)
  1086. assert.strictEqual(w.webContents.getTitle(), 'cat.pdf')
  1087. })
  1088. w.loadFile(path.join(fixtures, 'pages', page))
  1089. }
  1090. })
  1091. it('opens when loading a pdf resource as top level navigation', (done) => {
  1092. this.createBrowserWindow({ plugins: true, preload: 'preload-pdf-loaded.js' })
  1093. ipcMain.once('pdf-loaded', (event, state) => {
  1094. assert.strictEqual(state, 'success')
  1095. done()
  1096. })
  1097. w.webContents.on('page-title-updated', () => {
  1098. const parsedURL = url.parse(w.webContents.getURL(), true)
  1099. assert.strictEqual(parsedURL.protocol, 'chrome:')
  1100. assert.strictEqual(parsedURL.hostname, 'pdf-viewer')
  1101. assert.strictEqual(parsedURL.query.src, this.pdfSource)
  1102. assert.strictEqual(w.webContents.getTitle(), 'cat.pdf')
  1103. })
  1104. w.webContents.loadURL(this.pdfSource)
  1105. })
  1106. it('opens a pdf link given params, the query string should be escaped', (done) => {
  1107. this.createBrowserWindow({ plugins: true, preload: 'preload-pdf-loaded.js' })
  1108. ipcMain.once('pdf-loaded', (event, state) => {
  1109. assert.strictEqual(state, 'success')
  1110. done()
  1111. })
  1112. w.webContents.on('page-title-updated', () => {
  1113. const parsedURL = url.parse(w.webContents.getURL(), true)
  1114. assert.strictEqual(parsedURL.protocol, 'chrome:')
  1115. assert.strictEqual(parsedURL.hostname, 'pdf-viewer')
  1116. assert.strictEqual(parsedURL.query.src, this.pdfSourceWithParams)
  1117. assert.strictEqual(parsedURL.query.b, undefined)
  1118. assert(parsedURL.search.endsWith('%3Fa%3D1%26b%3D2'))
  1119. assert.strictEqual(w.webContents.getTitle(), 'cat.pdf')
  1120. })
  1121. w.webContents.loadURL(this.pdfSourceWithParams)
  1122. })
  1123. it('should download a pdf when plugins are disabled', (done) => {
  1124. this.createBrowserWindow({ plugins: false, preload: 'preload-pdf-loaded.js' })
  1125. ipcRenderer.sendSync('set-download-option', false, false)
  1126. ipcRenderer.once('download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) => {
  1127. assert.strictEqual(state, 'completed')
  1128. assert.strictEqual(filename, 'cat.pdf')
  1129. assert.strictEqual(mimeType, 'application/pdf')
  1130. fs.unlinkSync(path.join(fixtures, 'mock.pdf'))
  1131. done()
  1132. })
  1133. w.webContents.loadURL(this.pdfSource)
  1134. })
  1135. it('should not open when pdf is requested as sub resource', (done) => {
  1136. fetch(this.pdfSource).then((res) => {
  1137. assert.strictEqual(res.status, 200)
  1138. assert.notStrictEqual(document.title, 'cat.pdf')
  1139. done()
  1140. }).catch((e) => done(e))
  1141. })
  1142. it('opens when loading a pdf resource in a iframe', (done) => {
  1143. this.testPDFIsLoadedInSubFrame('pdf-in-iframe.html', 'preload-pdf-loaded-in-subframe.js', done)
  1144. })
  1145. it('opens when loading a pdf resource in a nested iframe', (done) => {
  1146. this.testPDFIsLoadedInSubFrame('pdf-in-nested-iframe.html', 'preload-pdf-loaded-in-nested-subframe.js', done)
  1147. })
  1148. })
  1149. describe('window.alert(message, title)', () => {
  1150. it('throws an exception when the arguments cannot be converted to strings', () => {
  1151. assert.throws(() => {
  1152. window.alert({ toString: null })
  1153. }, /Cannot convert object to primitive value/)
  1154. })
  1155. })
  1156. describe('window.confirm(message, title)', () => {
  1157. it('throws an exception when the arguments cannot be converted to strings', () => {
  1158. assert.throws(() => {
  1159. window.confirm({ toString: null }, 'title')
  1160. }, /Cannot convert object to primitive value/)
  1161. })
  1162. })
  1163. describe('window.history', () => {
  1164. describe('window.history.go(offset)', () => {
  1165. it('throws an exception when the argumnet cannot be converted to a string', () => {
  1166. assert.throws(() => {
  1167. window.history.go({ toString: null })
  1168. }, /Cannot convert object to primitive value/)
  1169. })
  1170. })
  1171. describe('window.history.pushState', () => {
  1172. it('should push state after calling history.pushState() from the same url', (done) => {
  1173. w = new BrowserWindow({ show: false })
  1174. w.webContents.once('did-finish-load', () => {
  1175. // History should have current page by now.
  1176. assert.strictEqual(w.webContents.length(), 1)
  1177. w.webContents.executeJavaScript('window.history.pushState({}, "")', () => {
  1178. // Initial page + pushed state
  1179. assert.strictEqual(w.webContents.length(), 2)
  1180. done()
  1181. })
  1182. })
  1183. w.loadURL('about:blank')
  1184. })
  1185. })
  1186. })
  1187. describe('SpeechSynthesis', () => {
  1188. before(function () {
  1189. if (isCI || !features.isTtsEnabled()) {
  1190. this.skip()
  1191. }
  1192. })
  1193. it('should emit lifecycle events', async () => {
  1194. const sentence = `long sentence which will take at least a few seconds to
  1195. utter so that it's possible to pause and resume before the end`
  1196. const utter = new SpeechSynthesisUtterance(sentence)
  1197. // Create a dummy utterence so that speech synthesis state
  1198. // is initialized for later calls.
  1199. speechSynthesis.speak(new SpeechSynthesisUtterance())
  1200. speechSynthesis.cancel()
  1201. speechSynthesis.speak(utter)
  1202. // paused state after speak()
  1203. expect(speechSynthesis.paused).to.be.false()
  1204. await new Promise((resolve) => { utter.onstart = resolve })
  1205. // paused state after start event
  1206. expect(speechSynthesis.paused).to.be.false()
  1207. speechSynthesis.pause()
  1208. // paused state changes async, right before the pause event
  1209. expect(speechSynthesis.paused).to.be.false()
  1210. await new Promise((resolve) => { utter.onpause = resolve })
  1211. expect(speechSynthesis.paused).to.be.true()
  1212. speechSynthesis.resume()
  1213. await new Promise((resolve) => { utter.onresume = resolve })
  1214. // paused state after resume event
  1215. expect(speechSynthesis.paused).to.be.false()
  1216. await new Promise((resolve) => { utter.onend = resolve })
  1217. })
  1218. })
  1219. })