webview-spec.ts 30 KB

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