api-browser-view-spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import { expect } from 'chai';
  2. import * as path from 'path';
  3. import { emittedOnce } from './events-helpers';
  4. import { BrowserView, BrowserWindow, screen, webContents } from 'electron/main';
  5. import { closeWindow } from './window-helpers';
  6. import { defer, ifit, startRemoteControlApp } from './spec-helpers';
  7. import { areColorsSimilar, captureScreen, getPixelColor } from './screen-helpers';
  8. describe('BrowserView module', () => {
  9. const fixtures = path.resolve(__dirname, '..', 'spec', 'fixtures');
  10. let w: BrowserWindow;
  11. let view: BrowserView;
  12. beforeEach(() => {
  13. expect(webContents.getAllWebContents()).to.have.length(0);
  14. w = new BrowserWindow({
  15. show: false,
  16. width: 400,
  17. height: 400,
  18. webPreferences: {
  19. backgroundThrottling: false
  20. }
  21. });
  22. });
  23. afterEach(async () => {
  24. const p = emittedOnce(w.webContents, 'destroyed');
  25. await closeWindow(w);
  26. w = null as any;
  27. await p;
  28. if (view) {
  29. const p = emittedOnce(view.webContents, 'destroyed');
  30. (view.webContents as any).destroy();
  31. view = null as any;
  32. await p;
  33. }
  34. expect(webContents.getAllWebContents()).to.have.length(0);
  35. });
  36. it('can be created with an existing webContents', async () => {
  37. const wc = (webContents as any).create({ sandbox: true });
  38. await wc.loadURL('about:blank');
  39. view = new BrowserView({ webContents: wc } as any);
  40. expect(view.webContents.getURL()).to.equal('about:blank');
  41. });
  42. describe('BrowserView.setBackgroundColor()', () => {
  43. it('does not throw for valid args', () => {
  44. view = new BrowserView();
  45. view.setBackgroundColor('#000');
  46. });
  47. it('throws for invalid args', () => {
  48. view = new BrowserView();
  49. expect(() => {
  50. view.setBackgroundColor(null as any);
  51. }).to.throw(/conversion failure/);
  52. });
  53. // Linux and arm64 platforms (WOA and macOS) do not return any capture sources
  54. ifit(process.platform !== 'linux' && process.arch !== 'arm64')('sets the background color to transparent if none is set', async () => {
  55. const display = screen.getPrimaryDisplay();
  56. const WINDOW_BACKGROUND_COLOR = '#55ccbb';
  57. w.show();
  58. w.setBounds(display.bounds);
  59. w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
  60. await w.loadURL('about:blank');
  61. view = new BrowserView();
  62. view.setBounds(display.bounds);
  63. w.setBrowserView(view);
  64. await view.webContents.loadURL('data:text/html,hello there');
  65. const screenCapture = await captureScreen();
  66. const centerColor = getPixelColor(screenCapture, {
  67. x: display.size.width / 2,
  68. y: display.size.height / 2
  69. });
  70. expect(areColorsSimilar(centerColor, WINDOW_BACKGROUND_COLOR)).to.be.true();
  71. });
  72. // Linux and arm64 platforms (WOA and macOS) do not return any capture sources
  73. ifit(process.platform !== 'linux' && process.arch !== 'arm64')('successfully applies the background color', async () => {
  74. const WINDOW_BACKGROUND_COLOR = '#55ccbb';
  75. const VIEW_BACKGROUND_COLOR = '#ff00ff';
  76. const display = screen.getPrimaryDisplay();
  77. w.show();
  78. w.setBounds(display.bounds);
  79. w.setBackgroundColor(WINDOW_BACKGROUND_COLOR);
  80. await w.loadURL('about:blank');
  81. view = new BrowserView();
  82. view.setBounds(display.bounds);
  83. w.setBrowserView(view);
  84. w.setBackgroundColor(VIEW_BACKGROUND_COLOR);
  85. await view.webContents.loadURL('data:text/html,hello there');
  86. const screenCapture = await captureScreen();
  87. const centerColor = getPixelColor(screenCapture, {
  88. x: display.size.width / 2,
  89. y: display.size.height / 2
  90. });
  91. expect(areColorsSimilar(centerColor, VIEW_BACKGROUND_COLOR)).to.be.true();
  92. });
  93. });
  94. describe('BrowserView.setAutoResize()', () => {
  95. it('does not throw for valid args', () => {
  96. view = new BrowserView();
  97. view.setAutoResize({});
  98. view.setAutoResize({ width: true, height: false });
  99. });
  100. it('throws for invalid args', () => {
  101. view = new BrowserView();
  102. expect(() => {
  103. view.setAutoResize(null as any);
  104. }).to.throw(/conversion failure/);
  105. });
  106. });
  107. describe('BrowserView.setBounds()', () => {
  108. it('does not throw for valid args', () => {
  109. view = new BrowserView();
  110. view.setBounds({ x: 0, y: 0, width: 1, height: 1 });
  111. });
  112. it('throws for invalid args', () => {
  113. view = new BrowserView();
  114. expect(() => {
  115. view.setBounds(null as any);
  116. }).to.throw(/conversion failure/);
  117. expect(() => {
  118. view.setBounds({} as any);
  119. }).to.throw(/conversion failure/);
  120. });
  121. });
  122. describe('BrowserView.getBounds()', () => {
  123. it('returns the current bounds', () => {
  124. view = new BrowserView();
  125. const bounds = { x: 10, y: 20, width: 30, height: 40 };
  126. view.setBounds(bounds);
  127. expect(view.getBounds()).to.deep.equal(bounds);
  128. });
  129. });
  130. describe('BrowserWindow.setBrowserView()', () => {
  131. it('does not throw for valid args', () => {
  132. view = new BrowserView();
  133. w.setBrowserView(view);
  134. });
  135. it('does not throw if called multiple times with same view', () => {
  136. view = new BrowserView();
  137. w.setBrowserView(view);
  138. w.setBrowserView(view);
  139. w.setBrowserView(view);
  140. });
  141. });
  142. describe('BrowserWindow.getBrowserView()', () => {
  143. it('returns the set view', () => {
  144. view = new BrowserView();
  145. w.setBrowserView(view);
  146. const view2 = w.getBrowserView();
  147. expect(view2!.webContents.id).to.equal(view.webContents.id);
  148. });
  149. it('returns null if none is set', () => {
  150. const view = w.getBrowserView();
  151. expect(view).to.be.null('view');
  152. });
  153. });
  154. describe('BrowserWindow.addBrowserView()', () => {
  155. it('does not throw for valid args', () => {
  156. const view1 = new BrowserView();
  157. defer(() => (view1.webContents as any).destroy());
  158. w.addBrowserView(view1);
  159. defer(() => w.removeBrowserView(view1));
  160. const view2 = new BrowserView();
  161. defer(() => (view2.webContents as any).destroy());
  162. w.addBrowserView(view2);
  163. defer(() => w.removeBrowserView(view2));
  164. });
  165. it('does not throw if called multiple times with same view', () => {
  166. view = new BrowserView();
  167. w.addBrowserView(view);
  168. w.addBrowserView(view);
  169. w.addBrowserView(view);
  170. });
  171. it('does not crash if the BrowserView webContents are destroyed prior to window addition', () => {
  172. expect(() => {
  173. const view1 = new BrowserView();
  174. (view1.webContents as any).destroy();
  175. w.addBrowserView(view1);
  176. }).to.not.throw();
  177. });
  178. it('does not crash if the webContents is destroyed after a URL is loaded', () => {
  179. view = new BrowserView();
  180. expect(async () => {
  181. view.setBounds({ x: 0, y: 0, width: 400, height: 300 });
  182. await view.webContents.loadURL('data:text/html,hello there');
  183. view.webContents.destroy();
  184. }).to.not.throw();
  185. });
  186. it('can handle BrowserView reparenting', async () => {
  187. view = new BrowserView();
  188. w.addBrowserView(view);
  189. view.webContents.loadURL('about:blank');
  190. await emittedOnce(view.webContents, 'did-finish-load');
  191. const w2 = new BrowserWindow({ show: false });
  192. w2.addBrowserView(view);
  193. w.close();
  194. view.webContents.loadURL(`file://${fixtures}/pages/blank.html`);
  195. await emittedOnce(view.webContents, 'did-finish-load');
  196. // Clean up - the afterEach hook assumes the webContents on w is still alive.
  197. w = new BrowserWindow({ show: false });
  198. w2.close();
  199. w2.destroy();
  200. });
  201. });
  202. describe('BrowserWindow.removeBrowserView()', () => {
  203. it('does not throw if called multiple times with same view', () => {
  204. expect(() => {
  205. view = new BrowserView();
  206. w.addBrowserView(view);
  207. w.removeBrowserView(view);
  208. w.removeBrowserView(view);
  209. }).to.not.throw();
  210. });
  211. });
  212. describe('BrowserWindow.getBrowserViews()', () => {
  213. it('returns same views as was added', () => {
  214. const view1 = new BrowserView();
  215. defer(() => (view1.webContents as any).destroy());
  216. w.addBrowserView(view1);
  217. defer(() => w.removeBrowserView(view1));
  218. const view2 = new BrowserView();
  219. defer(() => (view2.webContents as any).destroy());
  220. w.addBrowserView(view2);
  221. defer(() => w.removeBrowserView(view2));
  222. const views = w.getBrowserViews();
  223. expect(views).to.have.lengthOf(2);
  224. expect(views[0].webContents.id).to.equal(view1.webContents.id);
  225. expect(views[1].webContents.id).to.equal(view2.webContents.id);
  226. });
  227. });
  228. describe('BrowserWindow.setTopBrowserView()', () => {
  229. it('should throw an error when a BrowserView is not attached to the window', () => {
  230. view = new BrowserView();
  231. expect(() => {
  232. w.setTopBrowserView(view);
  233. }).to.throw(/is not attached/);
  234. });
  235. it('should throw an error when a BrowserView is attached to some other window', () => {
  236. view = new BrowserView();
  237. const win2 = new BrowserWindow();
  238. w.addBrowserView(view);
  239. view.setBounds({ x: 0, y: 0, width: 100, height: 100 });
  240. win2.addBrowserView(view);
  241. expect(() => {
  242. w.setTopBrowserView(view);
  243. }).to.throw(/is not attached/);
  244. win2.close();
  245. win2.destroy();
  246. });
  247. });
  248. describe('BrowserView.webContents.getOwnerBrowserWindow()', () => {
  249. it('points to owning window', () => {
  250. view = new BrowserView();
  251. expect(view.webContents.getOwnerBrowserWindow()).to.be.null('owner browser window');
  252. w.setBrowserView(view);
  253. expect(view.webContents.getOwnerBrowserWindow()).to.equal(w);
  254. w.setBrowserView(null);
  255. expect(view.webContents.getOwnerBrowserWindow()).to.be.null('owner browser window');
  256. });
  257. });
  258. describe('shutdown behavior', () => {
  259. it('does not crash on exit', async () => {
  260. const rc = await startRemoteControlApp();
  261. await rc.remotely(() => {
  262. const { BrowserView, app } = require('electron');
  263. new BrowserView({}) // eslint-disable-line
  264. setTimeout(() => {
  265. app.quit();
  266. });
  267. });
  268. const [code] = await emittedOnce(rc.process, 'exit');
  269. expect(code).to.equal(0);
  270. });
  271. it('does not crash on exit if added to a browser window', async () => {
  272. const rc = await startRemoteControlApp();
  273. await rc.remotely(() => {
  274. const { app, BrowserView, BrowserWindow } = require('electron');
  275. const bv = new BrowserView();
  276. bv.webContents.loadURL('about:blank');
  277. const bw = new BrowserWindow({ show: false });
  278. bw.addBrowserView(bv);
  279. setTimeout(() => {
  280. app.quit();
  281. });
  282. });
  283. const [code] = await emittedOnce(rc.process, 'exit');
  284. expect(code).to.equal(0);
  285. });
  286. });
  287. describe('window.open()', () => {
  288. it('works in BrowserView', (done) => {
  289. view = new BrowserView();
  290. w.setBrowserView(view);
  291. view.webContents.setWindowOpenHandler(({ url, frameName }) => {
  292. expect(url).to.equal('http://host/');
  293. expect(frameName).to.equal('host');
  294. done();
  295. return { action: 'deny' };
  296. });
  297. view.webContents.loadFile(path.join(fixtures, 'pages', 'window-open.html'));
  298. });
  299. });
  300. describe('BrowserView.capturePage(rect)', () => {
  301. it('returns a Promise with a Buffer', async () => {
  302. view = new BrowserView({
  303. webPreferences: {
  304. backgroundThrottling: false
  305. }
  306. });
  307. w.addBrowserView(view);
  308. view.setBounds({
  309. ...w.getBounds(),
  310. x: 0,
  311. y: 0
  312. });
  313. const image = await view.webContents.capturePage({
  314. x: 0,
  315. y: 0,
  316. width: 100,
  317. height: 100
  318. });
  319. expect(image.isEmpty()).to.equal(true);
  320. });
  321. xit('resolves after the window is hidden and capturer count is non-zero', async () => {
  322. view = new BrowserView({
  323. webPreferences: {
  324. backgroundThrottling: false
  325. }
  326. });
  327. w.setBrowserView(view);
  328. view.setBounds({
  329. ...w.getBounds(),
  330. x: 0,
  331. y: 0
  332. });
  333. await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
  334. view.webContents.incrementCapturerCount();
  335. const image = await view.webContents.capturePage();
  336. expect(image.isEmpty()).to.equal(false);
  337. });
  338. it('should increase the capturer count', () => {
  339. view = new BrowserView({
  340. webPreferences: {
  341. backgroundThrottling: false
  342. }
  343. });
  344. w.setBrowserView(view);
  345. view.setBounds({
  346. ...w.getBounds(),
  347. x: 0,
  348. y: 0
  349. });
  350. view.webContents.incrementCapturerCount();
  351. expect(view.webContents.isBeingCaptured()).to.be.true();
  352. view.webContents.decrementCapturerCount();
  353. expect(view.webContents.isBeingCaptured()).to.be.false();
  354. });
  355. });
  356. });