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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import { closeAllWindows } from './lib/window-helpers';
  2. import { expect } from 'chai';
  3. import { BaseWindow, View, WebContentsView, webContents } from 'electron/main';
  4. import { once } from 'node:events';
  5. import { defer } from './lib/spec-helpers';
  6. import { BrowserWindow } from 'electron';
  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. it('is initially hidden', async () => {
  114. const v = new WebContentsView();
  115. await v.webContents.loadURL('data:text/html,<script>initialVisibility = document.visibilityState</script>');
  116. expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('hidden');
  117. });
  118. it('becomes visible when attached', async () => {
  119. const v = new WebContentsView();
  120. await v.webContents.loadURL('about:blank');
  121. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  122. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  123. // Ensure that the above listener has been registered before we add the
  124. // view to the window, or else the visibilitychange event might be
  125. // dispatched before the listener is registered.
  126. // executeJavaScript calls are sequential so if this one's finished then
  127. // the previous one must also have been finished :)
  128. await v.webContents.executeJavaScript('undefined');
  129. const w = new BaseWindow();
  130. w.setContentView(v);
  131. await p;
  132. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  133. });
  134. it('is initially visible if load happens after attach', async () => {
  135. const w = new BaseWindow();
  136. const v = new WebContentsView();
  137. w.contentView = v;
  138. await v.webContents.loadURL('data:text/html,<script>initialVisibility = document.visibilityState</script>');
  139. expect(await v.webContents.executeJavaScript('initialVisibility')).to.equal('visible');
  140. });
  141. it('becomes hidden when parent window is hidden', async () => {
  142. const w = new BaseWindow();
  143. const v = new WebContentsView();
  144. w.setContentView(v);
  145. await v.webContents.loadURL('about:blank');
  146. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  147. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  148. // We have to wait until the listener above is fully registered before hiding the window.
  149. // On Windows, the executeJavaScript and the visibilitychange can happen out of order
  150. // without this.
  151. await v.webContents.executeJavaScript('0');
  152. w.hide();
  153. await p;
  154. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  155. });
  156. it('becomes visible when parent window is shown', async () => {
  157. const w = new BaseWindow({ show: false });
  158. const v = new WebContentsView();
  159. w.setContentView(v);
  160. await v.webContents.loadURL('about:blank');
  161. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('hidden');
  162. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", resolve))');
  163. // We have to wait until the listener above is fully registered before hiding the window.
  164. // On Windows, the executeJavaScript and the visibilitychange can happen out of order
  165. // without this.
  166. await v.webContents.executeJavaScript('0');
  167. w.show();
  168. await p;
  169. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  170. });
  171. it('does not change when view is moved between two visible windows', async () => {
  172. const w = new BaseWindow();
  173. const v = new WebContentsView();
  174. w.setContentView(v);
  175. await v.webContents.loadURL('about:blank');
  176. expect(await v.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
  177. const p = v.webContents.executeJavaScript('new Promise(resolve => document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))');
  178. // Ensure the listener has been registered.
  179. await v.webContents.executeJavaScript('undefined');
  180. const w2 = new BaseWindow();
  181. w2.setContentView(v);
  182. // Wait for the visibility state to settle as "visible".
  183. // On macOS one visibilitychange event is fired but visibilityState
  184. // remains "visible". On Win/Linux, two visibilitychange events are
  185. // fired, a "hidden" and a "visible" one. Reconcile these two models
  186. // by waiting until at least one event has been fired, and then waiting
  187. // until the visibility state settles as "visible".
  188. let visibilityState = await p;
  189. for (let attempts = 0; visibilityState !== 'visible' && attempts < 10; attempts++) {
  190. visibilityState = await v.webContents.executeJavaScript('new Promise(resolve => document.visibilityState === "visible" ? resolve("visible") : document.addEventListener("visibilitychange", () => resolve(document.visibilityState)))');
  191. }
  192. expect(visibilityState).to.equal('visible');
  193. });
  194. });
  195. });