webview-spec.ts 29 KB

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