webview-spec.ts 29 KB

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