api-web-contents-spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import * as chai from 'chai'
  2. import { AddressInfo } from 'net'
  3. import * as chaiAsPromised from 'chai-as-promised'
  4. import * as path from 'path'
  5. import * as http from 'http'
  6. import { BrowserWindow, ipcMain, webContents, session } from 'electron'
  7. import { emittedOnce } from './events-helpers';
  8. import { closeAllWindows } from './window-helpers';
  9. import { ifdescribe, ifit } from './spec-helpers';
  10. const { expect } = chai
  11. chai.use(chaiAsPromised)
  12. const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures')
  13. const features = process.electronBinding('features')
  14. describe('webContents module', () => {
  15. describe('getAllWebContents() API', () => {
  16. afterEach(closeAllWindows)
  17. it('returns an array of web contents', async () => {
  18. const w = new BrowserWindow({
  19. show: false,
  20. webPreferences: { webviewTag: true }
  21. })
  22. w.loadFile(path.join(fixturesPath, 'pages', 'webview-zoom-factor.html'))
  23. await emittedOnce(w.webContents, 'did-attach-webview')
  24. w.webContents.openDevTools()
  25. await emittedOnce(w.webContents, 'devtools-opened')
  26. const all = webContents.getAllWebContents().sort((a, b) => {
  27. return a.id - b.id
  28. })
  29. expect(all).to.have.length(3)
  30. expect(all[0].getType()).to.equal('window')
  31. expect(all[all.length - 2].getType()).to.equal('webview')
  32. expect(all[all.length - 1].getType()).to.equal('remote')
  33. })
  34. })
  35. describe('will-prevent-unload event', () => {
  36. afterEach(closeAllWindows)
  37. it('does not emit if beforeunload returns undefined', (done) => {
  38. const w = new BrowserWindow({show: false})
  39. w.once('closed', () => done())
  40. w.webContents.once('will-prevent-unload', (e) => {
  41. expect.fail('should not have fired')
  42. })
  43. w.loadFile(path.join(fixturesPath, 'api', 'close-beforeunload-undefined.html'))
  44. })
  45. it('emits if beforeunload returns false', (done) => {
  46. const w = new BrowserWindow({show: false})
  47. w.webContents.once('will-prevent-unload', () => done())
  48. w.loadFile(path.join(fixturesPath, 'api', 'close-beforeunload-false.html'))
  49. })
  50. it('supports calling preventDefault on will-prevent-unload events', (done) => {
  51. const w = new BrowserWindow({show: false})
  52. w.webContents.once('will-prevent-unload', event => event.preventDefault())
  53. w.once('closed', () => done())
  54. w.loadFile(path.join(fixturesPath, 'api', 'close-beforeunload-false.html'))
  55. })
  56. })
  57. describe('webContents.send(channel, args...)', () => {
  58. afterEach(closeAllWindows)
  59. it('throws an error when the channel is missing', () => {
  60. const w = new BrowserWindow({show: false})
  61. expect(() => {
  62. (w.webContents.send as any)()
  63. }).to.throw('Missing required channel argument')
  64. expect(() => {
  65. w.webContents.send(null as any)
  66. }).to.throw('Missing required channel argument')
  67. })
  68. it('does not block node async APIs when sent before document is ready', (done) => {
  69. // Please reference https://github.com/electron/electron/issues/19368 if
  70. // this test fails.
  71. ipcMain.once('async-node-api-done', () => {
  72. done()
  73. })
  74. const w = new BrowserWindow({
  75. show: false,
  76. webPreferences: {
  77. nodeIntegration: true,
  78. sandbox: false,
  79. contextIsolation: false
  80. }
  81. })
  82. w.loadFile(path.join(fixturesPath, 'pages', 'send-after-node.html'))
  83. setTimeout(() => {
  84. w.webContents.send("test")
  85. }, 50)
  86. })
  87. })
  88. ifdescribe(features.isPrintingEnabled())('webContents.print()', () => {
  89. let w: BrowserWindow
  90. beforeEach(() => {
  91. w = new BrowserWindow({ show: false })
  92. })
  93. afterEach(closeAllWindows)
  94. it('throws when invalid settings are passed', () => {
  95. expect(() => {
  96. // @ts-ignore this line is intentionally incorrect
  97. w.webContents.print(true)
  98. }).to.throw('webContents.print(): Invalid print settings specified.')
  99. })
  100. it('throws when an invalid callback is passed', () => {
  101. expect(() => {
  102. // @ts-ignore this line is intentionally incorrect
  103. w.webContents.print({}, true)
  104. }).to.throw('webContents.print(): Invalid optional callback provided.')
  105. })
  106. ifit(process.platform !== 'linux')('throws when an invalid deviceName is passed', () => {
  107. expect(() => {
  108. w.webContents.print({ deviceName: 'i-am-a-nonexistent-printer' }, () => {})
  109. }).to.throw('webContents.print(): Invalid deviceName provided.')
  110. })
  111. it('does not crash with custom margins', () => {
  112. expect(() => {
  113. w.webContents.print({
  114. silent: true,
  115. margins: {
  116. marginType: 'custom',
  117. top: 1,
  118. bottom: 1,
  119. left: 1,
  120. right: 1
  121. }
  122. })
  123. }).to.not.throw()
  124. })
  125. })
  126. describe('webContents.executeJavaScript', () => {
  127. describe('in about:blank', () => {
  128. const expected = 'hello, world!'
  129. const expectedErrorMsg = 'woops!'
  130. const code = `(() => "${expected}")()`
  131. const asyncCode = `(() => new Promise(r => setTimeout(() => r("${expected}"), 500)))()`
  132. const badAsyncCode = `(() => new Promise((r, e) => setTimeout(() => e("${expectedErrorMsg}"), 500)))()`
  133. const errorTypes = new Set([
  134. Error,
  135. ReferenceError,
  136. EvalError,
  137. RangeError,
  138. SyntaxError,
  139. TypeError,
  140. URIError
  141. ])
  142. let w: BrowserWindow
  143. before(async () => {
  144. w = new BrowserWindow({show: false})
  145. await w.loadURL('about:blank')
  146. })
  147. after(closeAllWindows)
  148. it('resolves the returned promise with the result', async () => {
  149. const result = await w.webContents.executeJavaScript(code)
  150. expect(result).to.equal(expected)
  151. })
  152. it('resolves the returned promise with the result if the code returns an asyncronous promise', async () => {
  153. const result = await w.webContents.executeJavaScript(asyncCode)
  154. expect(result).to.equal(expected)
  155. })
  156. it('rejects the returned promise if an async error is thrown', async () => {
  157. await expect(w.webContents.executeJavaScript(badAsyncCode)).to.eventually.be.rejectedWith(expectedErrorMsg)
  158. })
  159. it('rejects the returned promise with an error if an Error.prototype is thrown', async () => {
  160. for (const error of errorTypes) {
  161. await expect(w.webContents.executeJavaScript(`Promise.reject(new ${error.name}("Wamp-wamp"))`))
  162. .to.eventually.be.rejectedWith(error)
  163. }
  164. })
  165. })
  166. describe("on a real page", () => {
  167. let w: BrowserWindow
  168. beforeEach(() => {
  169. w = new BrowserWindow({show: false})
  170. })
  171. afterEach(closeAllWindows)
  172. let server: http.Server = null as unknown as http.Server
  173. let serverUrl: string = null as unknown as string
  174. before((done) => {
  175. server = http.createServer((request, response) => {
  176. response.end()
  177. }).listen(0, '127.0.0.1', () => {
  178. serverUrl = 'http://127.0.0.1:' + (server.address() as AddressInfo).port
  179. done()
  180. })
  181. })
  182. after(() => {
  183. server.close()
  184. })
  185. it('works after page load and during subframe load', (done) => {
  186. w.webContents.once('did-finish-load', () => {
  187. // initiate a sub-frame load, then try and execute script during it
  188. w.webContents.executeJavaScript(`
  189. var iframe = document.createElement('iframe')
  190. iframe.src = '${serverUrl}/slow'
  191. document.body.appendChild(iframe)
  192. `).then(() => {
  193. w.webContents.executeJavaScript('console.log(\'hello\')').then(() => {
  194. done()
  195. })
  196. })
  197. })
  198. w.loadURL(serverUrl)
  199. })
  200. it('executes after page load', (done) => {
  201. w.webContents.executeJavaScript(`(() => "test")()`).then(result => {
  202. expect(result).to.equal("test")
  203. done()
  204. })
  205. w.loadURL(serverUrl)
  206. })
  207. it('works with result objects that have DOM class prototypes', (done) => {
  208. w.webContents.executeJavaScript('document.location').then(result => {
  209. expect(result.origin).to.equal(serverUrl)
  210. expect(result.protocol).to.equal('http:')
  211. done()
  212. })
  213. w.loadURL(serverUrl)
  214. })
  215. })
  216. })
  217. describe('login event', () => {
  218. afterEach(closeAllWindows)
  219. let server: http.Server
  220. let serverUrl: string
  221. let serverPort: number
  222. let proxyServer: http.Server
  223. let proxyServerPort: number
  224. before((done) => {
  225. server = http.createServer((request, response) => {
  226. if (request.url === '/no-auth') {
  227. return response.end('ok')
  228. }
  229. if (request.headers.authorization) {
  230. response.writeHead(200, { 'Content-type': 'text/plain' })
  231. return response.end(request.headers.authorization)
  232. }
  233. response
  234. .writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' })
  235. .end('401')
  236. }).listen(0, '127.0.0.1', () => {
  237. serverPort = (server.address() as AddressInfo).port
  238. serverUrl = `http://127.0.0.1:${serverPort}`
  239. done()
  240. })
  241. })
  242. before((done) => {
  243. proxyServer = http.createServer((request, response) => {
  244. if (request.headers['proxy-authorization']) {
  245. response.writeHead(200, { 'Content-type': 'text/plain' })
  246. return response.end(request.headers['proxy-authorization'])
  247. }
  248. response
  249. .writeHead(407, { 'Proxy-Authenticate': 'Basic realm="Foo"' })
  250. .end()
  251. }).listen(0, '127.0.0.1', () => {
  252. proxyServerPort = (proxyServer.address() as AddressInfo).port
  253. done()
  254. })
  255. })
  256. afterEach(async () => {
  257. await session.defaultSession.clearAuthCache({ type: 'password' })
  258. })
  259. after(() => {
  260. server.close()
  261. proxyServer.close()
  262. })
  263. it('is emitted when navigating', async () => {
  264. const [user, pass] = ['user', 'pass']
  265. const w = new BrowserWindow({ show: false })
  266. let eventRequest: any
  267. let eventAuthInfo: any
  268. w.webContents.on('login', (event, request, authInfo, cb) => {
  269. eventRequest = request
  270. eventAuthInfo = authInfo
  271. event.preventDefault()
  272. cb(user, pass)
  273. })
  274. await w.loadURL(serverUrl)
  275. const body = await w.webContents.executeJavaScript(`document.documentElement.textContent`)
  276. expect(body).to.equal(`Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`)
  277. expect(eventRequest.url).to.equal(serverUrl + '/')
  278. expect(eventAuthInfo.isProxy).to.be.false('is proxy')
  279. expect(eventAuthInfo.scheme).to.equal('basic')
  280. expect(eventAuthInfo.host).to.equal('127.0.0.1')
  281. expect(eventAuthInfo.port).to.equal(serverPort)
  282. expect(eventAuthInfo.realm).to.equal('Foo')
  283. })
  284. it('is emitted when a proxy requests authorization', async () => {
  285. const customSession = session.fromPartition(`${Math.random()}`)
  286. await customSession.setProxy({ proxyRules: `127.0.0.1:${proxyServerPort}`, proxyBypassRules: '<-loopback>' } as any)
  287. const [user, pass] = ['user', 'pass']
  288. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } })
  289. let eventRequest: any
  290. let eventAuthInfo: any
  291. w.webContents.on('login', (event, request, authInfo, cb) => {
  292. eventRequest = request
  293. eventAuthInfo = authInfo
  294. event.preventDefault()
  295. cb(user, pass)
  296. })
  297. await w.loadURL(`${serverUrl}/no-auth`)
  298. const body = await w.webContents.executeJavaScript(`document.documentElement.textContent`)
  299. expect(body).to.equal(`Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`)
  300. expect(eventRequest.url).to.equal(`${serverUrl}/no-auth`)
  301. expect(eventAuthInfo.isProxy).to.be.true('is proxy')
  302. expect(eventAuthInfo.scheme).to.equal('basic')
  303. expect(eventAuthInfo.host).to.equal('127.0.0.1')
  304. expect(eventAuthInfo.port).to.equal(proxyServerPort)
  305. expect(eventAuthInfo.realm).to.equal('Foo')
  306. })
  307. it('cancels authentication when callback is called with no arguments', async () => {
  308. const w = new BrowserWindow({ show: false })
  309. w.webContents.on('login', (event, request, authInfo, cb) => {
  310. event.preventDefault()
  311. cb()
  312. })
  313. await w.loadURL(serverUrl)
  314. const body = await w.webContents.executeJavaScript(`document.documentElement.textContent`)
  315. expect(body).to.equal('401')
  316. })
  317. })
  318. it('emits a cancelable event before creating a child webcontents', async () => {
  319. const w = new BrowserWindow({
  320. show: false,
  321. webPreferences: {
  322. sandbox: true
  323. }
  324. })
  325. w.webContents.on('-will-add-new-contents' as any, (event: any, url: any) => {
  326. expect(url).to.equal('about:blank')
  327. event.preventDefault()
  328. })
  329. let wasCalled = false
  330. w.webContents.on('new-window' as any, () => {
  331. wasCalled = true
  332. })
  333. await w.loadURL('about:blank')
  334. await w.webContents.executeJavaScript(`window.open('about:blank')`)
  335. await new Promise((resolve) => { process.nextTick(resolve) })
  336. expect(wasCalled).to.equal(false)
  337. await closeAllWindows()
  338. })
  339. })