webview-spec.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. import * as path from 'path';
  2. import * as url from 'url';
  3. import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main';
  4. import { closeAllWindows } from './window-helpers';
  5. import { emittedOnce, emittedUntil } from './events-helpers';
  6. import { ifit, delay } from './spec-helpers';
  7. import { expect } from 'chai';
  8. async function loadWebView (w: WebContents, attributes: Record<string, string>, openDevTools: boolean = false): Promise<void> {
  9. await w.executeJavaScript(`
  10. new Promise((resolve, reject) => {
  11. const webview = new WebView()
  12. for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) {
  13. webview.setAttribute(k, v)
  14. }
  15. document.body.appendChild(webview)
  16. webview.addEventListener('dom-ready', () => {
  17. if (${openDevTools}) {
  18. webview.openDevTools()
  19. }
  20. })
  21. webview.addEventListener('did-finish-load', () => {
  22. resolve()
  23. })
  24. })
  25. `);
  26. }
  27. describe('<webview> tag', function () {
  28. const fixtures = path.join(__dirname, '..', 'spec', 'fixtures');
  29. afterEach(closeAllWindows);
  30. function hideChildWindows (e: any, wc: WebContents) {
  31. wc.on('new-window', (event, url, frameName, disposition, options) => {
  32. options.show = false;
  33. });
  34. }
  35. before(() => {
  36. app.on('web-contents-created', hideChildWindows);
  37. });
  38. after(() => {
  39. app.off('web-contents-created', hideChildWindows);
  40. });
  41. it('works without script tag in page', async () => {
  42. const w = new BrowserWindow({
  43. show: false,
  44. webPreferences: {
  45. webviewTag: true,
  46. nodeIntegration: true,
  47. contextIsolation: false
  48. }
  49. });
  50. w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html'));
  51. await emittedOnce(ipcMain, 'pong');
  52. });
  53. it('works with sandbox', async () => {
  54. const w = new BrowserWindow({
  55. show: false,
  56. webPreferences: {
  57. webviewTag: true,
  58. sandbox: true
  59. }
  60. });
  61. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  62. await emittedOnce(ipcMain, 'pong');
  63. });
  64. it('works with contextIsolation', async () => {
  65. const w = new BrowserWindow({
  66. show: false,
  67. webPreferences: {
  68. webviewTag: true,
  69. contextIsolation: true
  70. }
  71. });
  72. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  73. await emittedOnce(ipcMain, 'pong');
  74. });
  75. it('works with contextIsolation + sandbox', async () => {
  76. const w = new BrowserWindow({
  77. show: false,
  78. webPreferences: {
  79. webviewTag: true,
  80. contextIsolation: true,
  81. sandbox: true
  82. }
  83. });
  84. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  85. await emittedOnce(ipcMain, 'pong');
  86. });
  87. it('works with Trusted Types', async () => {
  88. const w = new BrowserWindow({
  89. show: false,
  90. webPreferences: {
  91. webviewTag: true
  92. }
  93. });
  94. w.loadFile(path.join(fixtures, 'pages', 'webview-trusted-types.html'));
  95. await emittedOnce(ipcMain, 'pong');
  96. });
  97. it('is disabled by default', async () => {
  98. const w = new BrowserWindow({
  99. show: false,
  100. webPreferences: {
  101. preload: path.join(fixtures, 'module', 'preload-webview.js'),
  102. nodeIntegration: true
  103. }
  104. });
  105. const webview = emittedOnce(ipcMain, 'webview');
  106. w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html'));
  107. const [, type] = await webview;
  108. expect(type).to.equal('undefined', 'WebView still exists');
  109. });
  110. // FIXME(deepak1556): Ch69 follow up.
  111. xdescribe('document.visibilityState/hidden', () => {
  112. afterEach(() => {
  113. ipcMain.removeAllListeners('pong');
  114. });
  115. it('updates when the window is shown after the ready-to-show event', async () => {
  116. const w = new BrowserWindow({ show: false });
  117. const readyToShowSignal = emittedOnce(w, 'ready-to-show');
  118. const pongSignal1 = emittedOnce(ipcMain, 'pong');
  119. w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html'));
  120. await pongSignal1;
  121. const pongSignal2 = emittedOnce(ipcMain, 'pong');
  122. await readyToShowSignal;
  123. w.show();
  124. const [, visibilityState, hidden] = await pongSignal2;
  125. expect(visibilityState).to.equal('visible');
  126. expect(hidden).to.be.false();
  127. });
  128. it('inherits the parent window visibility state and receives visibilitychange events', async () => {
  129. const w = new BrowserWindow({ show: false });
  130. w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html'));
  131. const [, visibilityState, hidden] = await emittedOnce(ipcMain, 'pong');
  132. expect(visibilityState).to.equal('hidden');
  133. expect(hidden).to.be.true();
  134. // We have to start waiting for the event
  135. // before we ask the webContents to resize.
  136. const getResponse = emittedOnce(ipcMain, 'pong');
  137. w.webContents.emit('-window-visibility-change', 'visible');
  138. return getResponse.then(([, visibilityState, hidden]) => {
  139. expect(visibilityState).to.equal('visible');
  140. expect(hidden).to.be.false();
  141. });
  142. });
  143. });
  144. describe('did-attach-webview event', () => {
  145. it('is emitted when a webview has been attached', async () => {
  146. const w = new BrowserWindow({
  147. show: false,
  148. webPreferences: {
  149. webviewTag: true,
  150. nodeIntegration: true,
  151. contextIsolation: false
  152. }
  153. });
  154. const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview');
  155. const webviewDomReady = emittedOnce(ipcMain, 'webview-dom-ready');
  156. w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html'));
  157. const [, webContents] = await didAttachWebview;
  158. const [, id] = await webviewDomReady;
  159. expect(webContents.id).to.equal(id);
  160. });
  161. });
  162. describe('did-attach event', () => {
  163. it('is emitted when a webview has been attached', async () => {
  164. const w = new BrowserWindow({
  165. webPreferences: {
  166. webviewTag: true
  167. }
  168. });
  169. await w.loadURL('about:blank');
  170. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  171. const webview = new WebView()
  172. webview.setAttribute('src', 'about:blank')
  173. webview.addEventListener('did-attach', (e) => {
  174. resolve('ok')
  175. })
  176. document.body.appendChild(webview)
  177. })`);
  178. expect(message).to.equal('ok');
  179. });
  180. });
  181. describe('did-change-theme-color event', () => {
  182. it('emits when theme color changes', async () => {
  183. const w = new BrowserWindow({
  184. webPreferences: {
  185. webviewTag: true
  186. }
  187. });
  188. await w.loadURL('about:blank');
  189. const src = url.format({
  190. pathname: `${fixtures.replace(/\\/g, '/')}/pages/theme-color.html`,
  191. protocol: 'file',
  192. slashes: true
  193. });
  194. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  195. const webview = new WebView()
  196. webview.setAttribute('src', '${src}')
  197. webview.addEventListener('did-change-theme-color', (e) => {
  198. resolve('ok')
  199. })
  200. document.body.appendChild(webview)
  201. })`);
  202. expect(message).to.equal('ok');
  203. });
  204. });
  205. it('loads devtools extensions registered on the parent window', async () => {
  206. const w = new BrowserWindow({
  207. show: false,
  208. webPreferences: {
  209. webviewTag: true,
  210. nodeIntegration: true,
  211. contextIsolation: false
  212. }
  213. });
  214. w.webContents.session.removeExtension('foo');
  215. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
  216. await w.webContents.session.loadExtension(extensionPath);
  217. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'webview-devtools.html'));
  218. loadWebView(w.webContents, {
  219. nodeintegration: 'on',
  220. webpreferences: 'contextIsolation=no',
  221. src: `file://${path.join(__dirname, 'fixtures', 'blank.html')}`
  222. }, true);
  223. let childWebContentsId = 0;
  224. app.once('web-contents-created', (e, webContents) => {
  225. childWebContentsId = webContents.id;
  226. webContents.on('devtools-opened', function () {
  227. const showPanelIntervalId = setInterval(function () {
  228. if (!webContents.isDestroyed() && webContents.devToolsWebContents) {
  229. webContents.devToolsWebContents.executeJavaScript('(' + function () {
  230. const { UI } = (window as any);
  231. const tabs = UI.inspectorView._tabbedPane._tabs;
  232. const lastPanelId: any = tabs[tabs.length - 1].id;
  233. UI.inspectorView.showPanel(lastPanelId);
  234. }.toString() + ')()');
  235. } else {
  236. clearInterval(showPanelIntervalId);
  237. }
  238. }, 100);
  239. });
  240. });
  241. const [, { runtimeId, tabId }] = await emittedOnce(ipcMain, 'answer');
  242. expect(runtimeId).to.match(/^[a-z]{32}$/);
  243. expect(tabId).to.equal(childWebContentsId);
  244. });
  245. describe('zoom behavior', () => {
  246. const zoomScheme = standardScheme;
  247. const webviewSession = session.fromPartition('webview-temp');
  248. before(() => {
  249. const protocol = webviewSession.protocol;
  250. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  251. callback('hello');
  252. });
  253. });
  254. after(() => {
  255. const protocol = webviewSession.protocol;
  256. protocol.unregisterProtocol(zoomScheme);
  257. });
  258. it('inherits the zoomFactor of the parent window', async () => {
  259. const w = new BrowserWindow({
  260. show: false,
  261. webPreferences: {
  262. webviewTag: true,
  263. nodeIntegration: true,
  264. zoomFactor: 1.2,
  265. contextIsolation: false
  266. }
  267. });
  268. const zoomEventPromise = emittedOnce(ipcMain, 'webview-parent-zoom-level');
  269. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-factor.html'));
  270. const [, zoomFactor, zoomLevel] = await zoomEventPromise;
  271. expect(zoomFactor).to.equal(1.2);
  272. expect(zoomLevel).to.equal(1);
  273. });
  274. it('maintains zoom level on navigation', async () => {
  275. const w = new BrowserWindow({
  276. show: false,
  277. webPreferences: {
  278. webviewTag: true,
  279. nodeIntegration: true,
  280. zoomFactor: 1.2,
  281. contextIsolation: false
  282. }
  283. });
  284. const promise = new Promise<void>((resolve) => {
  285. ipcMain.on('webview-zoom-level', (event, zoomLevel, zoomFactor, newHost, final) => {
  286. if (!newHost) {
  287. expect(zoomFactor).to.equal(1.44);
  288. expect(zoomLevel).to.equal(2.0);
  289. } else {
  290. expect(zoomFactor).to.equal(1.2);
  291. expect(zoomLevel).to.equal(1);
  292. }
  293. if (final) {
  294. resolve();
  295. }
  296. });
  297. });
  298. w.loadFile(path.join(fixtures, 'pages', 'webview-custom-zoom-level.html'));
  299. await promise;
  300. });
  301. it('maintains zoom level when navigating within same page', async () => {
  302. const w = new BrowserWindow({
  303. show: false,
  304. webPreferences: {
  305. webviewTag: true,
  306. nodeIntegration: true,
  307. zoomFactor: 1.2,
  308. contextIsolation: false
  309. }
  310. });
  311. const promise = new Promise<void>((resolve) => {
  312. ipcMain.on('webview-zoom-in-page', (event, zoomLevel, zoomFactor, final) => {
  313. expect(zoomFactor).to.equal(1.44);
  314. expect(zoomLevel).to.equal(2.0);
  315. if (final) {
  316. resolve();
  317. }
  318. });
  319. });
  320. w.loadFile(path.join(fixtures, 'pages', 'webview-in-page-navigate.html'));
  321. await promise;
  322. });
  323. it('inherits zoom level for the origin when available', async () => {
  324. const w = new BrowserWindow({
  325. show: false,
  326. webPreferences: {
  327. webviewTag: true,
  328. nodeIntegration: true,
  329. zoomFactor: 1.2,
  330. contextIsolation: false
  331. }
  332. });
  333. w.loadFile(path.join(fixtures, 'pages', 'webview-origin-zoom-level.html'));
  334. const [, zoomLevel] = await emittedOnce(ipcMain, 'webview-origin-zoom-level');
  335. expect(zoomLevel).to.equal(2.0);
  336. });
  337. it('does not crash when navigating with zoom level inherited from parent', async () => {
  338. const w = new BrowserWindow({
  339. show: false,
  340. webPreferences: {
  341. webviewTag: true,
  342. nodeIntegration: true,
  343. zoomFactor: 1.2,
  344. session: webviewSession,
  345. contextIsolation: false
  346. }
  347. });
  348. const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
  349. const readyPromise = emittedOnce(ipcMain, 'dom-ready');
  350. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html'));
  351. const [, webview] = await attachPromise;
  352. await readyPromise;
  353. expect(webview.getZoomFactor()).to.equal(1.2);
  354. await w.loadURL(`${zoomScheme}://host1`);
  355. });
  356. });
  357. describe('requestFullscreen from webview', () => {
  358. const loadWebViewWindow = async () => {
  359. const w = new BrowserWindow({
  360. webPreferences: {
  361. webviewTag: true,
  362. nodeIntegration: true,
  363. contextIsolation: false
  364. }
  365. });
  366. const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
  367. const readyPromise = emittedOnce(ipcMain, 'webview-ready');
  368. w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html'));
  369. const [, webview] = await attachPromise;
  370. await readyPromise;
  371. return [w, webview];
  372. };
  373. afterEach(closeAllWindows);
  374. it('should make parent frame element fullscreen too', async () => {
  375. const [w, webview] = await loadWebViewWindow();
  376. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
  377. const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
  378. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  379. await parentFullscreen;
  380. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
  381. });
  382. // FIXME(zcbenz): Fullscreen events do not work on Linux.
  383. // This test is flaky on arm64 macOS.
  384. ifit(process.platform !== 'linux' && process.arch !== 'arm64')('exiting fullscreen should unfullscreen window', async () => {
  385. const [w, webview] = await loadWebViewWindow();
  386. const enterFullScreen = emittedOnce(w, 'enter-full-screen');
  387. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  388. await enterFullScreen;
  389. const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
  390. await webview.executeJavaScript('document.exitFullscreen()', true);
  391. await leaveFullScreen;
  392. await delay(0);
  393. expect(w.isFullScreen()).to.be.false();
  394. });
  395. // Sending ESC via sendInputEvent only works on Windows.
  396. ifit(process.platform === 'win32')('pressing ESC should unfullscreen window', async () => {
  397. const [w, webview] = await loadWebViewWindow();
  398. const enterFullScreen = emittedOnce(w, 'enter-full-screen');
  399. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  400. await enterFullScreen;
  401. const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
  402. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
  403. await leaveFullScreen;
  404. await delay(0);
  405. expect(w.isFullScreen()).to.be.false();
  406. });
  407. });
  408. describe('nativeWindowOpen option', () => {
  409. let w: BrowserWindow;
  410. beforeEach(async () => {
  411. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  412. await w.loadURL('about:blank');
  413. });
  414. afterEach(closeAllWindows);
  415. it('opens window of about:blank with cross-scripting enabled', async () => {
  416. // Don't wait for loading to finish.
  417. loadWebView(w.webContents, {
  418. allowpopups: 'on',
  419. nodeintegration: 'on',
  420. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  421. src: `file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}`
  422. });
  423. const [, content] = await emittedOnce(ipcMain, 'answer');
  424. expect(content).to.equal('Hello');
  425. });
  426. it('opens window of same domain with cross-scripting enabled', async () => {
  427. // Don't wait for loading to finish.
  428. loadWebView(w.webContents, {
  429. allowpopups: 'on',
  430. nodeintegration: 'on',
  431. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  432. src: `file://${path.join(fixtures, 'api', 'native-window-open-file.html')}`
  433. });
  434. const [, content] = await emittedOnce(ipcMain, 'answer');
  435. expect(content).to.equal('Hello');
  436. });
  437. it('returns null from window.open when allowpopups is not set', async () => {
  438. // Don't wait for loading to finish.
  439. loadWebView(w.webContents, {
  440. nodeintegration: 'on',
  441. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  442. src: `file://${path.join(fixtures, 'api', 'native-window-open-no-allowpopups.html')}`
  443. });
  444. const [, { windowOpenReturnedNull }] = await emittedOnce(ipcMain, 'answer');
  445. expect(windowOpenReturnedNull).to.be.true();
  446. });
  447. it('blocks accessing cross-origin frames', async () => {
  448. // Don't wait for loading to finish.
  449. loadWebView(w.webContents, {
  450. allowpopups: 'on',
  451. nodeintegration: 'on',
  452. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  453. src: `file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}`
  454. });
  455. const [, content] = await emittedOnce(ipcMain, 'answer');
  456. const expectedContent =
  457. 'Blocked a frame with origin "file://" from accessing a cross-origin frame.';
  458. expect(content).to.equal(expectedContent);
  459. });
  460. it('emits a new-window event', async () => {
  461. // Don't wait for loading to finish.
  462. const attributes = {
  463. allowpopups: 'on',
  464. nodeintegration: 'on',
  465. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  466. src: `file://${fixtures}/pages/window-open.html`
  467. };
  468. const { url, frameName } = await w.webContents.executeJavaScript(`
  469. new Promise((resolve, reject) => {
  470. const webview = document.createElement('webview')
  471. for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) {
  472. webview.setAttribute(k, v)
  473. }
  474. document.body.appendChild(webview)
  475. webview.addEventListener('new-window', (e) => {
  476. resolve({url: e.url, frameName: e.frameName})
  477. })
  478. })
  479. `);
  480. expect(url).to.equal('http://host/');
  481. expect(frameName).to.equal('host');
  482. });
  483. it('emits a browser-window-created event', async () => {
  484. // Don't wait for loading to finish.
  485. loadWebView(w.webContents, {
  486. allowpopups: 'on',
  487. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  488. src: `file://${fixtures}/pages/window-open.html`
  489. });
  490. await emittedOnce(app, 'browser-window-created');
  491. });
  492. it('emits a web-contents-created event', async () => {
  493. const webContentsCreated = emittedUntil(app, 'web-contents-created',
  494. (event: Electron.Event, contents: Electron.WebContents) => contents.getType() === 'window');
  495. loadWebView(w.webContents, {
  496. allowpopups: 'on',
  497. webpreferences: 'nativeWindowOpen=1,contextIsolation=no',
  498. src: `file://${fixtures}/pages/window-open.html`
  499. });
  500. await webContentsCreated;
  501. });
  502. it('does not crash when creating window with noopener', async () => {
  503. loadWebView(w.webContents, {
  504. allowpopups: 'on',
  505. webpreferences: 'nativeWindowOpen=1',
  506. src: `file://${path.join(fixtures, 'api', 'native-window-open-noopener.html')}`
  507. });
  508. await emittedOnce(app, 'browser-window-created');
  509. });
  510. });
  511. describe('webpreferences attribute', () => {
  512. let w: BrowserWindow;
  513. beforeEach(async () => {
  514. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true } });
  515. await w.loadURL('about:blank');
  516. });
  517. afterEach(closeAllWindows);
  518. it('can enable context isolation', async () => {
  519. loadWebView(w.webContents, {
  520. allowpopups: 'yes',
  521. preload: `file://${fixtures}/api/isolated-preload.js`,
  522. src: `file://${fixtures}/api/isolated.html`,
  523. webpreferences: 'contextIsolation=yes'
  524. });
  525. const [, data] = await emittedOnce(ipcMain, 'isolated-world');
  526. expect(data).to.deep.equal({
  527. preloadContext: {
  528. preloadProperty: 'number',
  529. pageProperty: 'undefined',
  530. typeofRequire: 'function',
  531. typeofProcess: 'object',
  532. typeofArrayPush: 'function',
  533. typeofFunctionApply: 'function',
  534. typeofPreloadExecuteJavaScriptProperty: 'undefined'
  535. },
  536. pageContext: {
  537. preloadProperty: 'undefined',
  538. pageProperty: 'string',
  539. typeofRequire: 'undefined',
  540. typeofProcess: 'undefined',
  541. typeofArrayPush: 'number',
  542. typeofFunctionApply: 'boolean',
  543. typeofPreloadExecuteJavaScriptProperty: 'number',
  544. typeofOpenedWindow: 'object'
  545. }
  546. });
  547. });
  548. });
  549. describe('permission request handlers', () => {
  550. let w: BrowserWindow;
  551. beforeEach(async () => {
  552. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  553. await w.loadURL('about:blank');
  554. });
  555. afterEach(closeAllWindows);
  556. const partition = 'permissionTest';
  557. function setUpRequestHandler (webContentsId: number, requestedPermission: string) {
  558. return new Promise<void>((resolve, reject) => {
  559. session.fromPartition(partition).setPermissionRequestHandler(function (webContents, permission, callback) {
  560. if (webContents.id === webContentsId) {
  561. // requestMIDIAccess with sysex requests both midi and midiSysex so
  562. // grant the first midi one and then reject the midiSysex one
  563. if (requestedPermission === 'midiSysex' && permission === 'midi') {
  564. return callback(true);
  565. }
  566. try {
  567. expect(permission).to.equal(requestedPermission);
  568. } catch (e) {
  569. return reject(e);
  570. }
  571. callback(false);
  572. resolve();
  573. }
  574. });
  575. });
  576. }
  577. afterEach(() => {
  578. session.fromPartition(partition).setPermissionRequestHandler(null);
  579. });
  580. // This is disabled because CI machines don't have cameras or microphones,
  581. // so Chrome responds with "NotFoundError" instead of
  582. // "PermissionDeniedError". It should be re-enabled if we find a way to mock
  583. // the presence of a microphone & camera.
  584. xit('emits when using navigator.getUserMedia api', async () => {
  585. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  586. loadWebView(w.webContents, {
  587. src: `file://${fixtures}/pages/permissions/media.html`,
  588. partition,
  589. nodeintegration: 'on'
  590. });
  591. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  592. setUpRequestHandler(webViewContents.id, 'media');
  593. const [, errorName] = await errorFromRenderer;
  594. expect(errorName).to.equal('PermissionDeniedError');
  595. });
  596. it('emits when using navigator.geolocation api', async () => {
  597. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  598. loadWebView(w.webContents, {
  599. src: `file://${fixtures}/pages/permissions/geolocation.html`,
  600. partition,
  601. nodeintegration: 'on',
  602. webpreferences: 'contextIsolation=no'
  603. });
  604. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  605. setUpRequestHandler(webViewContents.id, 'geolocation');
  606. const [, error] = await errorFromRenderer;
  607. expect(error).to.equal('User denied Geolocation');
  608. });
  609. it('emits when using navigator.requestMIDIAccess without sysex api', async () => {
  610. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  611. loadWebView(w.webContents, {
  612. src: `file://${fixtures}/pages/permissions/midi.html`,
  613. partition,
  614. nodeintegration: 'on',
  615. webpreferences: 'contextIsolation=no'
  616. });
  617. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  618. setUpRequestHandler(webViewContents.id, 'midi');
  619. const [, error] = await errorFromRenderer;
  620. expect(error).to.equal('SecurityError');
  621. });
  622. it('emits when using navigator.requestMIDIAccess with sysex api', async () => {
  623. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  624. loadWebView(w.webContents, {
  625. src: `file://${fixtures}/pages/permissions/midi-sysex.html`,
  626. partition,
  627. nodeintegration: 'on',
  628. webpreferences: 'contextIsolation=no'
  629. });
  630. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  631. setUpRequestHandler(webViewContents.id, 'midiSysex');
  632. const [, error] = await errorFromRenderer;
  633. expect(error).to.equal('SecurityError');
  634. });
  635. it('emits when accessing external protocol', async () => {
  636. loadWebView(w.webContents, {
  637. src: 'magnet:test',
  638. partition
  639. });
  640. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  641. await setUpRequestHandler(webViewContents.id, 'openExternal');
  642. });
  643. it('emits when using Notification.requestPermission', async () => {
  644. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  645. loadWebView(w.webContents, {
  646. src: `file://${fixtures}/pages/permissions/notification.html`,
  647. partition,
  648. nodeintegration: 'on',
  649. webpreferences: 'contextIsolation=no'
  650. });
  651. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  652. await setUpRequestHandler(webViewContents.id, 'notifications');
  653. const [, error] = await errorFromRenderer;
  654. expect(error).to.equal('denied');
  655. });
  656. });
  657. describe('DOM events', () => {
  658. afterEach(closeAllWindows);
  659. it('receives extra properties on DOM events when contextIsolation is enabled', async () => {
  660. const w = new BrowserWindow({
  661. show: false,
  662. webPreferences: {
  663. webviewTag: true,
  664. contextIsolation: true
  665. }
  666. });
  667. await w.loadURL('about:blank');
  668. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  669. const webview = new WebView()
  670. webview.setAttribute('src', 'data:text/html,<script>console.log("hi")</script>')
  671. webview.addEventListener('console-message', (e) => {
  672. resolve(e.message)
  673. })
  674. document.body.appendChild(webview)
  675. })`);
  676. expect(message).to.equal('hi');
  677. });
  678. it('emits focus event when contextIsolation is enabled', async () => {
  679. const w = new BrowserWindow({
  680. show: false,
  681. webPreferences: {
  682. webviewTag: true,
  683. contextIsolation: true
  684. }
  685. });
  686. await w.loadURL('about:blank');
  687. await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  688. const webview = new WebView()
  689. webview.setAttribute('src', 'about:blank')
  690. webview.addEventListener('dom-ready', () => {
  691. webview.focus()
  692. })
  693. webview.addEventListener('focus', () => {
  694. resolve();
  695. })
  696. document.body.appendChild(webview)
  697. })`);
  698. });
  699. });
  700. });