webview-spec.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  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. // This test is flaky on WOA, so skip it there.
  206. ifit(process.platform !== 'win32' || process.arch !== 'arm64')('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. w.webContents.session.removeExtension('foo');
  216. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
  217. await w.webContents.session.loadExtension(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: any = 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<void>((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<void>((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. it('does not crash when changing zoom level after webview is destroyed', async () => {
  358. const w = new BrowserWindow({
  359. show: false,
  360. webPreferences: {
  361. webviewTag: true,
  362. nodeIntegration: true,
  363. session: webviewSession,
  364. contextIsolation: false
  365. }
  366. });
  367. const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
  368. await w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html'));
  369. await attachPromise;
  370. await w.webContents.executeJavaScript('view.remove()');
  371. w.webContents.setZoomLevel(0.5);
  372. });
  373. });
  374. describe('requestFullscreen from webview', () => {
  375. const loadWebViewWindow = async () => {
  376. const w = new BrowserWindow({
  377. webPreferences: {
  378. webviewTag: true,
  379. nodeIntegration: true,
  380. contextIsolation: false
  381. }
  382. });
  383. const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
  384. const loadPromise = emittedOnce(w.webContents, 'did-finish-load');
  385. const readyPromise = emittedOnce(ipcMain, 'webview-ready');
  386. w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html'));
  387. const [, webview] = await attachPromise;
  388. await Promise.all([readyPromise, loadPromise]);
  389. return [w, webview];
  390. };
  391. afterEach(async () => {
  392. // The leaving animation is un-observable but can interfere with future tests
  393. // Specifically this is async on macOS but can be on other platforms too
  394. await delay(1000);
  395. closeAllWindows();
  396. });
  397. ifit(process.platform !== 'darwin')('should make parent frame element fullscreen too (non-macOS)', async () => {
  398. const [w, webview] = await loadWebViewWindow();
  399. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
  400. const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
  401. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  402. await parentFullscreen;
  403. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
  404. const close = emittedOnce(w, 'closed');
  405. w.close();
  406. await close;
  407. });
  408. ifit(process.platform === 'darwin')('should make parent frame element fullscreen too (macOS)', async () => {
  409. const [w, webview] = await loadWebViewWindow();
  410. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
  411. const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
  412. const enterHTMLFS = emittedOnce(w.webContents, 'enter-html-full-screen');
  413. const leaveHTMLFS = emittedOnce(w.webContents, 'leave-html-full-screen');
  414. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  415. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
  416. await webview.executeJavaScript('document.exitFullscreen()');
  417. await Promise.all([enterHTMLFS, leaveHTMLFS, parentFullscreen]);
  418. const close = emittedOnce(w, 'closed');
  419. w.close();
  420. await close;
  421. });
  422. // FIXME(zcbenz): Fullscreen events do not work on Linux.
  423. ifit(process.platform !== 'linux')('exiting fullscreen should unfullscreen window', async () => {
  424. const [w, webview] = await loadWebViewWindow();
  425. const enterFullScreen = emittedOnce(w, 'enter-full-screen');
  426. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  427. await enterFullScreen;
  428. const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
  429. await webview.executeJavaScript('document.exitFullscreen()', true);
  430. await leaveFullScreen;
  431. await delay(0);
  432. expect(w.isFullScreen()).to.be.false();
  433. const close = emittedOnce(w, 'closed');
  434. w.close();
  435. await close;
  436. });
  437. // Sending ESC via sendInputEvent only works on Windows.
  438. ifit(process.platform === 'win32')('pressing ESC should unfullscreen window', async () => {
  439. const [w, webview] = await loadWebViewWindow();
  440. const enterFullScreen = emittedOnce(w, 'enter-full-screen');
  441. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  442. await enterFullScreen;
  443. const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
  444. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
  445. await leaveFullScreen;
  446. await delay(0);
  447. expect(w.isFullScreen()).to.be.false();
  448. const close = emittedOnce(w, 'closed');
  449. w.close();
  450. await close;
  451. });
  452. it('pressing ESC should emit the leave-html-full-screen event', async () => {
  453. const w = new BrowserWindow({
  454. show: false,
  455. webPreferences: {
  456. webviewTag: true,
  457. nodeIntegration: true,
  458. contextIsolation: false
  459. }
  460. });
  461. const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview');
  462. w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html'));
  463. const [, webContents] = await didAttachWebview;
  464. const enterFSWindow = emittedOnce(w, 'enter-html-full-screen');
  465. const enterFSWebview = emittedOnce(webContents, 'enter-html-full-screen');
  466. await webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  467. await enterFSWindow;
  468. await enterFSWebview;
  469. const leaveFSWindow = emittedOnce(w, 'leave-html-full-screen');
  470. const leaveFSWebview = emittedOnce(webContents, 'leave-html-full-screen');
  471. webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
  472. await leaveFSWebview;
  473. await leaveFSWindow;
  474. const close = emittedOnce(w, 'closed');
  475. w.close();
  476. await close;
  477. });
  478. it('should support user gesture', async () => {
  479. const [w, webview] = await loadWebViewWindow();
  480. const waitForEnterHtmlFullScreen = emittedOnce(webview, 'enter-html-full-screen');
  481. const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
  482. webview.executeJavaScript(jsScript, true);
  483. await waitForEnterHtmlFullScreen;
  484. const close = emittedOnce(w, 'closed');
  485. w.close();
  486. await close;
  487. });
  488. });
  489. describe('child windows', () => {
  490. let w: BrowserWindow;
  491. beforeEach(async () => {
  492. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  493. await w.loadURL('about:blank');
  494. });
  495. afterEach(closeAllWindows);
  496. it('opens window of about:blank with cross-scripting enabled', async () => {
  497. // Don't wait for loading to finish.
  498. loadWebView(w.webContents, {
  499. allowpopups: 'on',
  500. nodeintegration: 'on',
  501. webpreferences: 'contextIsolation=no',
  502. src: `file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}`
  503. });
  504. const [, content] = await emittedOnce(ipcMain, 'answer');
  505. expect(content).to.equal('Hello');
  506. });
  507. it('opens window of same domain with cross-scripting enabled', async () => {
  508. // Don't wait for loading to finish.
  509. loadWebView(w.webContents, {
  510. allowpopups: 'on',
  511. nodeintegration: 'on',
  512. webpreferences: 'contextIsolation=no',
  513. src: `file://${path.join(fixtures, 'api', 'native-window-open-file.html')}`
  514. });
  515. const [, content] = await emittedOnce(ipcMain, 'answer');
  516. expect(content).to.equal('Hello');
  517. });
  518. it('returns null from window.open when allowpopups is not set', async () => {
  519. // Don't wait for loading to finish.
  520. loadWebView(w.webContents, {
  521. nodeintegration: 'on',
  522. webpreferences: 'contextIsolation=no',
  523. src: `file://${path.join(fixtures, 'api', 'native-window-open-no-allowpopups.html')}`
  524. });
  525. const [, { windowOpenReturnedNull }] = await emittedOnce(ipcMain, 'answer');
  526. expect(windowOpenReturnedNull).to.be.true();
  527. });
  528. it('blocks accessing cross-origin frames', async () => {
  529. // Don't wait for loading to finish.
  530. loadWebView(w.webContents, {
  531. allowpopups: 'on',
  532. nodeintegration: 'on',
  533. webpreferences: 'contextIsolation=no',
  534. src: `file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}`
  535. });
  536. const [, content] = await emittedOnce(ipcMain, 'answer');
  537. const expectedContent =
  538. 'Blocked a frame with origin "file://" from accessing a cross-origin frame.';
  539. expect(content).to.equal(expectedContent);
  540. });
  541. it('emits a new-window event', async () => {
  542. // Don't wait for loading to finish.
  543. const attributes = {
  544. allowpopups: 'on',
  545. nodeintegration: 'on',
  546. webpreferences: 'contextIsolation=no',
  547. src: `file://${fixtures}/pages/window-open.html`
  548. };
  549. const { url, frameName } = await w.webContents.executeJavaScript(`
  550. new Promise((resolve, reject) => {
  551. const webview = document.createElement('webview')
  552. for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) {
  553. webview.setAttribute(k, v)
  554. }
  555. document.body.appendChild(webview)
  556. webview.addEventListener('new-window', (e) => {
  557. resolve({url: e.url, frameName: e.frameName})
  558. })
  559. })
  560. `);
  561. expect(url).to.equal('http://host/');
  562. expect(frameName).to.equal('host');
  563. });
  564. it('emits a browser-window-created event', async () => {
  565. // Don't wait for loading to finish.
  566. loadWebView(w.webContents, {
  567. allowpopups: 'on',
  568. webpreferences: 'contextIsolation=no',
  569. src: `file://${fixtures}/pages/window-open.html`
  570. });
  571. await emittedOnce(app, 'browser-window-created');
  572. });
  573. it('emits a web-contents-created event', async () => {
  574. const webContentsCreated = emittedUntil(app, 'web-contents-created',
  575. (event: Electron.Event, contents: Electron.WebContents) => contents.getType() === 'window');
  576. loadWebView(w.webContents, {
  577. allowpopups: 'on',
  578. webpreferences: 'contextIsolation=no',
  579. src: `file://${fixtures}/pages/window-open.html`
  580. });
  581. await webContentsCreated;
  582. });
  583. it('does not crash when creating window with noopener', async () => {
  584. loadWebView(w.webContents, {
  585. allowpopups: 'on',
  586. src: `file://${path.join(fixtures, 'api', 'native-window-open-noopener.html')}`
  587. });
  588. await emittedOnce(app, 'browser-window-created');
  589. });
  590. });
  591. describe('webpreferences attribute', () => {
  592. let w: BrowserWindow;
  593. beforeEach(async () => {
  594. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true } });
  595. await w.loadURL('about:blank');
  596. });
  597. afterEach(closeAllWindows);
  598. it('can enable context isolation', async () => {
  599. loadWebView(w.webContents, {
  600. allowpopups: 'yes',
  601. preload: `file://${fixtures}/api/isolated-preload.js`,
  602. src: `file://${fixtures}/api/isolated.html`,
  603. webpreferences: 'contextIsolation=yes'
  604. });
  605. const [, data] = await emittedOnce(ipcMain, 'isolated-world');
  606. expect(data).to.deep.equal({
  607. preloadContext: {
  608. preloadProperty: 'number',
  609. pageProperty: 'undefined',
  610. typeofRequire: 'function',
  611. typeofProcess: 'object',
  612. typeofArrayPush: 'function',
  613. typeofFunctionApply: 'function',
  614. typeofPreloadExecuteJavaScriptProperty: 'undefined'
  615. },
  616. pageContext: {
  617. preloadProperty: 'undefined',
  618. pageProperty: 'string',
  619. typeofRequire: 'undefined',
  620. typeofProcess: 'undefined',
  621. typeofArrayPush: 'number',
  622. typeofFunctionApply: 'boolean',
  623. typeofPreloadExecuteJavaScriptProperty: 'number',
  624. typeofOpenedWindow: 'object'
  625. }
  626. });
  627. });
  628. });
  629. describe('permission request handlers', () => {
  630. let w: BrowserWindow;
  631. beforeEach(async () => {
  632. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  633. await w.loadURL('about:blank');
  634. });
  635. afterEach(closeAllWindows);
  636. const partition = 'permissionTest';
  637. function setUpRequestHandler (webContentsId: number, requestedPermission: string) {
  638. return new Promise<void>((resolve, reject) => {
  639. session.fromPartition(partition).setPermissionRequestHandler(function (webContents, permission, callback) {
  640. if (webContents.id === webContentsId) {
  641. // requestMIDIAccess with sysex requests both midi and midiSysex so
  642. // grant the first midi one and then reject the midiSysex one
  643. if (requestedPermission === 'midiSysex' && permission === 'midi') {
  644. return callback(true);
  645. }
  646. try {
  647. expect(permission).to.equal(requestedPermission);
  648. } catch (e) {
  649. return reject(e);
  650. }
  651. callback(false);
  652. resolve();
  653. }
  654. });
  655. });
  656. }
  657. afterEach(() => {
  658. session.fromPartition(partition).setPermissionRequestHandler(null);
  659. });
  660. // This is disabled because CI machines don't have cameras or microphones,
  661. // so Chrome responds with "NotFoundError" instead of
  662. // "PermissionDeniedError". It should be re-enabled if we find a way to mock
  663. // the presence of a microphone & camera.
  664. xit('emits when using navigator.getUserMedia api', async () => {
  665. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  666. loadWebView(w.webContents, {
  667. src: `file://${fixtures}/pages/permissions/media.html`,
  668. partition,
  669. nodeintegration: 'on'
  670. });
  671. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  672. setUpRequestHandler(webViewContents.id, 'media');
  673. const [, errorName] = await errorFromRenderer;
  674. expect(errorName).to.equal('PermissionDeniedError');
  675. });
  676. it('emits when using navigator.geolocation api', async () => {
  677. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  678. loadWebView(w.webContents, {
  679. src: `file://${fixtures}/pages/permissions/geolocation.html`,
  680. partition,
  681. nodeintegration: 'on',
  682. webpreferences: 'contextIsolation=no'
  683. });
  684. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  685. setUpRequestHandler(webViewContents.id, 'geolocation');
  686. const [, error] = await errorFromRenderer;
  687. expect(error).to.equal('User denied Geolocation');
  688. });
  689. it('emits when using navigator.requestMIDIAccess without sysex api', async () => {
  690. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  691. loadWebView(w.webContents, {
  692. src: `file://${fixtures}/pages/permissions/midi.html`,
  693. partition,
  694. nodeintegration: 'on',
  695. webpreferences: 'contextIsolation=no'
  696. });
  697. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  698. setUpRequestHandler(webViewContents.id, 'midi');
  699. const [, error] = await errorFromRenderer;
  700. expect(error).to.equal('SecurityError');
  701. });
  702. it('emits when using navigator.requestMIDIAccess with sysex api', async () => {
  703. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  704. loadWebView(w.webContents, {
  705. src: `file://${fixtures}/pages/permissions/midi-sysex.html`,
  706. partition,
  707. nodeintegration: 'on',
  708. webpreferences: 'contextIsolation=no'
  709. });
  710. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  711. setUpRequestHandler(webViewContents.id, 'midiSysex');
  712. const [, error] = await errorFromRenderer;
  713. expect(error).to.equal('SecurityError');
  714. });
  715. it('emits when accessing external protocol', async () => {
  716. loadWebView(w.webContents, {
  717. src: 'magnet:test',
  718. partition
  719. });
  720. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  721. await setUpRequestHandler(webViewContents.id, 'openExternal');
  722. });
  723. it('emits when using Notification.requestPermission', async () => {
  724. const errorFromRenderer = emittedOnce(ipcMain, 'message');
  725. loadWebView(w.webContents, {
  726. src: `file://${fixtures}/pages/permissions/notification.html`,
  727. partition,
  728. nodeintegration: 'on',
  729. webpreferences: 'contextIsolation=no'
  730. });
  731. const [, webViewContents] = await emittedOnce(app, 'web-contents-created');
  732. await setUpRequestHandler(webViewContents.id, 'notifications');
  733. const [, error] = await errorFromRenderer;
  734. expect(error).to.equal('denied');
  735. });
  736. });
  737. describe('DOM events', () => {
  738. afterEach(closeAllWindows);
  739. it('receives extra properties on DOM events when contextIsolation is enabled', async () => {
  740. const w = new BrowserWindow({
  741. show: false,
  742. webPreferences: {
  743. webviewTag: true,
  744. contextIsolation: true
  745. }
  746. });
  747. await w.loadURL('about:blank');
  748. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  749. const webview = new WebView()
  750. webview.setAttribute('src', 'data:text/html,<script>console.log("hi")</script>')
  751. webview.addEventListener('console-message', (e) => {
  752. resolve(e.message)
  753. })
  754. document.body.appendChild(webview)
  755. })`);
  756. expect(message).to.equal('hi');
  757. });
  758. it('emits focus event when contextIsolation is enabled', async () => {
  759. const w = new BrowserWindow({
  760. show: false,
  761. webPreferences: {
  762. webviewTag: true,
  763. contextIsolation: true
  764. }
  765. });
  766. await w.loadURL('about:blank');
  767. await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  768. const webview = new WebView()
  769. webview.setAttribute('src', 'about:blank')
  770. webview.addEventListener('dom-ready', () => {
  771. webview.focus()
  772. })
  773. webview.addEventListener('focus', () => {
  774. resolve();
  775. })
  776. document.body.appendChild(webview)
  777. })`);
  778. });
  779. });
  780. });