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

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