api-web-contents-view-spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen } from 'electron/main';
  2. import { expect } from 'chai';
  3. import { once } from 'node:events';
  4. import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
  5. import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
  6. import { closeAllWindows } from './lib/window-helpers';
  7. describe('WebContentsView', () => {
  8. afterEach(async () => {
  9. await closeAllWindows();
  10. const existingWCS = webContents.getAllWebContents();
  11. existingWCS.forEach((contents) => contents.close());
  12. });
  13. it('can be instantiated with no arguments', () => {
  14. // eslint-disable-next-line no-new
  15. new WebContentsView();
  16. });
  17. it('can be instantiated with no webPreferences', () => {
  18. // eslint-disable-next-line no-new
  19. new WebContentsView({});
  20. });
  21. it('accepts existing webContents object', async () => {
  22. const currentWebContentsCount = webContents.getAllWebContents().length;
  23. const wc = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  24. defer(() => wc.destroy());
  25. await wc.loadURL('about:blank');
  26. const webContentsView = new WebContentsView({
  27. webContents: wc
  28. });
  29. expect(webContentsView.webContents).to.eq(wc);
  30. expect(webContents.getAllWebContents().length).to.equal(currentWebContentsCount + 1, 'expected only single webcontents to be created');
  31. });
  32. it('should throw error when created with already attached webContents to BrowserWindow', () => {
  33. const browserWindow = new BrowserWindow();
  34. defer(() => browserWindow.webContents.destroy());
  35. const webContentsView = new WebContentsView();
  36. defer(() => webContentsView.webContents.destroy());
  37. browserWindow.contentView.addChildView(webContentsView);
  38. defer(() => browserWindow.contentView.removeChildView(webContentsView));
  39. expect(() => new WebContentsView({
  40. webContents: webContentsView.webContents
  41. })).to.throw('options.webContents is already attached to a window');
  42. });
  43. it('should throw error when created with already attached webContents to other WebContentsView', () => {
  44. const browserWindow = new BrowserWindow();
  45. const webContentsView = new WebContentsView();
  46. defer(() => webContentsView.webContents.destroy());
  47. webContentsView.webContents.loadURL('about:blank');
  48. expect(() => new WebContentsView({
  49. webContents: browserWindow.webContents
  50. })).to.throw('options.webContents is already attached to a window');
  51. });
  52. it('can be used as content view', () => {
  53. const w = new BaseWindow({ show: false });
  54. w.setContentView(new WebContentsView());
  55. });
  56. it('can be removed after a close', async () => {
  57. const w = new BaseWindow({ show: false });
  58. const v = new View();
  59. const wcv = new WebContentsView();
  60. w.setContentView(v);
  61. v.addChildView(wcv);
  62. await wcv.webContents.loadURL('about:blank');
  63. const destroyed = once(wcv.webContents, 'destroyed');
  64. wcv.webContents.executeJavaScript('window.close()');
  65. await destroyed;
  66. expect(wcv.webContents.isDestroyed()).to.be.true();
  67. v.removeChildView(wcv);
  68. });
  69. it('correctly reorders children', () => {
  70. const w = new BaseWindow({ show: false });
  71. const cv = new View();
  72. w.setContentView(cv);
  73. const wcv1 = new WebContentsView();
  74. const wcv2 = new WebContentsView();
  75. const wcv3 = new WebContentsView();
  76. w.contentView.addChildView(wcv1);
  77. w.contentView.addChildView(wcv2);
  78. w.contentView.addChildView(wcv3);
  79. expect(w.contentView.children).to.deep.equal([wcv1, wcv2, wcv3]);
  80. w.contentView.addChildView(wcv1);
  81. w.contentView.addChildView(wcv2);
  82. expect(w.contentView.children).to.deep.equal([wcv3, wcv1, wcv2]);
  83. });
  84. it('handle removal and re-addition of children', () => {
  85. const w = new BaseWindow({ show: false });
  86. const cv = new View();
  87. w.setContentView(cv);
  88. const wcv1 = new WebContentsView();
  89. const wcv2 = new WebContentsView();
  90. expect(w.contentView.children).to.deep.equal([]);
  91. w.contentView.addChildView(wcv1);
  92. w.contentView.addChildView(wcv2);
  93. expect(w.contentView.children).to.deep.equal([wcv1, wcv2]);
  94. w.contentView.removeChildView(wcv1);
  95. expect(w.contentView.children).to.deep.equal([wcv2]);
  96. w.contentView.addChildView(wcv1);
  97. expect(w.contentView.children).to.deep.equal([wcv2, wcv1]);
  98. });
  99. function triggerGCByAllocation () {
  100. const arr = [];
  101. for (let i = 0; i < 1000000; i++) {
  102. arr.push([]);
  103. }
  104. return arr;
  105. }
  106. it('doesn\'t crash when GCed during allocation', (done) => {
  107. // eslint-disable-next-line no-new
  108. new WebContentsView();
  109. setTimeout(() => {
  110. // NB. the crash we're testing for is the lack of a current `v8::Context`
  111. // when emitting an event in WebContents's destructor. V8 is inconsistent
  112. // about whether or not there's a current context during garbage
  113. // collection, and it seems that `v8Util.requestGarbageCollectionForTesting`
  114. // causes a GC in which there _is_ a current context, so the crash isn't
  115. // triggered. Thus, we force a GC by other means: namely, by allocating a
  116. // bunch of stuff.
  117. triggerGCByAllocation();
  118. done();
  119. });
  120. });
  121. it('can be fullscreened', async () => {
  122. const w = new BaseWindow();
  123. const v = new WebContentsView();
  124. w.setContentView(v);
  125. await v.webContents.loadURL('data:text/html,<div id="div">This is a simple div.</div>');
  126. const enterFullScreen = once(w, 'enter-full-screen');
  127. await v.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  128. await enterFullScreen;
  129. expect(w.isFullScreen()).to.be.true('isFullScreen');
  130. });
  131. it('can be added as a child of another View', async () => {
  132. const w = new BaseWindow();
  133. const v = new View();
  134. const wcv = new WebContentsView();
  135. await wcv.webContents.loadURL('data:text/html,<div id="div">This is a simple div.</div>');
  136. v.addChildView(wcv);
  137. w.contentView.addChildView(v);
  138. expect(w.contentView.children).to.deep.equal([v]);
  139. expect(v.children).to.deep.equal([wcv]);
  140. });
  141. describe('visibilityState', () => {
  142. async function haveVisibilityState (view: WebContentsView, state: string) {
  143. const docVisState = await view.webContents.executeJavaScript('document.visibilityState');
  144. return docVisState === state;
  145. }
  146. it('is initially hidden', async () => {
  147. const v = new WebContentsView();
  148. await v.webContents.loadURL('data:text/html,<script>initialVisibility = document.visibilityState</script>');
  149. expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('hidden');
  150. });
  151. it('becomes visible when attached', async () => {
  152. const v = new WebContentsView();
  153. await v.webContents.loadURL('about:blank');
  154. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  155. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  156. // Ensure that the above listener has been registered before we add the
  157. // view to the window, or else the visibilitychange event might be
  158. // dispatched before the listener is registered.
  159. // executeJavaScript calls are sequential so if this one's finished then
  160. // the previous one must also have been finished :)
  161. await v.webContents.executeJavaScript('undefined');
  162. const w = new BaseWindow();
  163. w.setContentView(v);
  164. await p;
  165. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  166. });
  167. it('is initially visible if load happens after attach', async () => {
  168. const w = new BaseWindow();
  169. const v = new WebContentsView();
  170. w.contentView = v;
  171. await v.webContents.loadURL('data:text/html,<script>initialVisibility = document.visibilityState</script>');
  172. expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('visible');
  173. });
  174. it('becomes hidden when parent window is hidden', async () => {
  175. const w = new BaseWindow();
  176. const v = new WebContentsView();
  177. w.setContentView(v);
  178. await v.webContents.loadURL('about:blank');
  179. await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
  180. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  181. // We have to wait until the listener above is fully registered before hiding the window.
  182. // On Windows, the executeJavaScript and the visibilitychange can happen out of order
  183. // without this.
  184. await v.webContents.executeJavaScript('0');
  185. w.hide();
  186. await p;
  187. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  188. });
  189. it('becomes visible when parent window is shown', async () => {
  190. const w = new BaseWindow({ show: false });
  191. const v = new WebContentsView();
  192. w.setContentView(v);
  193. await v.webContents.loadURL('about:blank');
  194. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  195. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  196. // We have to wait until the listener above is fully registered before hiding the window.
  197. // On Windows, the executeJavaScript and the visibilitychange can happen out of order
  198. // without this.
  199. await v.webContents.executeJavaScript('0');
  200. w.show();
  201. await p;
  202. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  203. });
  204. it('does not change when view is moved between two visible windows', async () => {
  205. const w = new BaseWindow();
  206. const v = new WebContentsView();
  207. w.setContentView(v);
  208. await v.webContents.loadURL('about:blank');
  209. await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
  210. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))');
  211. // Ensure the listener has been registered.
  212. await v.webContents.executeJavaScript('undefined');
  213. const w2 = new BaseWindow();
  214. w2.setContentView(v);
  215. // Wait for the visibility state to settle as "visible".
  216. // On macOS one visibilitychange event is fired but visibilityState
  217. // remains "visible". On Win/Linux, two visibilitychange events are
  218. // fired, a "hidden" and a "visible" one. Reconcile these two models
  219. // by waiting until at least one event has been fired, and then waiting
  220. // until the visibility state settles as "visible".
  221. let visibilityState = await p;
  222. for (let attempts = 0; visibilityState !== 'visible' && attempts < 10; attempts++) {
  223. visibilityState = await v.webContents.executeJavaScript('new Promise(resolve => document.visibilityState === "visible" ? resolve("visible") : document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))');
  224. }
  225. expect(visibilityState).to.equal('visible');
  226. });
  227. });
  228. describe('setBorderRadius', () => {
  229. ifdescribe(hasCapturableScreen())('capture', () => {
  230. let w: Electron.BaseWindow;
  231. let v: Electron.WebContentsView;
  232. let display: Electron.Display;
  233. let corners: Electron.Point[];
  234. const backgroundUrl = `data:text/html,<style>html{background:${encodeURIComponent(HexColors.GREEN)}}</style>`;
  235. beforeEach(async () => {
  236. display = screen.getPrimaryDisplay();
  237. w = new BaseWindow({
  238. ...display.workArea,
  239. show: true,
  240. frame: false,
  241. hasShadow: false,
  242. backgroundColor: HexColors.BLUE,
  243. roundedCorners: false
  244. });
  245. v = new WebContentsView();
  246. w.setContentView(v);
  247. v.setBorderRadius(100);
  248. const readyForCapture = once(v.webContents, 'ready-to-show');
  249. v.webContents.loadURL(backgroundUrl);
  250. const inset = 10;
  251. corners = [
  252. { x: display.workArea.x + inset, y: display.workArea.y + inset }, // top-left
  253. { x: display.workArea.x + display.workArea.width - inset, y: display.workArea.y + inset }, // top-right
  254. { x: display.workArea.x + display.workArea.width - inset, y: display.workArea.y + display.workArea.height - inset }, // bottom-right
  255. { x: display.workArea.x + inset, y: display.workArea.y + display.workArea.height - inset } // bottom-left
  256. ];
  257. await readyForCapture;
  258. });
  259. afterEach(() => {
  260. w.destroy();
  261. w = v = null!;
  262. });
  263. it('should render with cutout corners', async () => {
  264. const screenCapture = new ScreenCapture(display);
  265. for (const corner of corners) {
  266. await screenCapture.expectColorAtPointOnDisplayMatches(HexColors.BLUE, () => corner);
  267. }
  268. // Center should be WebContents page background color
  269. await screenCapture.expectColorAtCenterMatches(HexColors.GREEN);
  270. });
  271. it('should allow resetting corners', async () => {
  272. const corner = corners[0];
  273. v.setBorderRadius(0);
  274. await nextFrameTime();
  275. const screenCapture = new ScreenCapture(display);
  276. await screenCapture.expectColorAtPointOnDisplayMatches(HexColors.GREEN, () => corner);
  277. await screenCapture.expectColorAtCenterMatches(HexColors.GREEN);
  278. });
  279. it('should render when set before attached', async () => {
  280. v = new WebContentsView();
  281. v.setBorderRadius(100); // must set before
  282. w.setContentView(v);
  283. const readyForCapture = once(v.webContents, 'ready-to-show');
  284. v.webContents.loadURL(backgroundUrl);
  285. await readyForCapture;
  286. const corner = corners[0];
  287. const screenCapture = new ScreenCapture(display);
  288. await screenCapture.expectColorAtPointOnDisplayMatches(HexColors.BLUE, () => corner);
  289. await screenCapture.expectColorAtCenterMatches(HexColors.GREEN);
  290. });
  291. });
  292. it('should allow setting when not attached', async () => {
  293. const v = new WebContentsView();
  294. v.setBorderRadius(100);
  295. });
  296. });
  297. });