Browse Source

feat: add webContents.close() (#35509)

* feat: add webContents.close()

* update docs, add test for beforeunload override

* Update web-contents.md
Jeremy Rose 2 years ago
parent
commit
eebf34cc6c

+ 15 - 0
docs/api/web-contents.md

@@ -934,6 +934,21 @@ Returns `string` - The title of the current web page.
 
 Returns `boolean` - Whether the web page is destroyed.
 
+#### `contents.close([opts])`
+
+* `opts` Object (optional)
+  * `waitForBeforeUnload` boolean - if true, fire the `beforeunload` event
+    before closing the page. If the page prevents the unload, the WebContents
+    will not be closed. The [`will-prevent-unload`](#event-will-prevent-unload)
+    will be fired if the page requests prevention of unload.
+
+Closes the page, as if the web content had called `window.close()`.
+
+If the page is successfully closed (i.e. the unload is not prevented by the
+page, or `waitForBeforeUnload` is false or unspecified), the WebContents will
+be destroyed and no longer usable. The [`destroyed`](#event-destroyed) event
+will be emitted.
+
 #### `contents.focus()`
 
 Focuses the web page.

+ 1 - 1
shell/browser/api/electron_api_browser_window.cc

@@ -134,6 +134,7 @@ BrowserWindow::~BrowserWindow() {
     api_web_contents_->RemoveObserver(this);
     // Destroy the WebContents.
     OnCloseContents();
+    api_web_contents_->Destroy();
   }
 }
 
@@ -181,7 +182,6 @@ void BrowserWindow::WebContentsDestroyed() {
 
 void BrowserWindow::OnCloseContents() {
   BaseWindow::ResetBrowserViews();
-  api_web_contents_->Destroy();
 }
 
 void BrowserWindow::OnRendererResponsive(content::RenderProcessHost*) {

+ 16 - 0
shell/browser/api/electron_api_web_contents.cc

@@ -1009,6 +1009,19 @@ void WebContents::Destroy() {
   }
 }
 
+void WebContents::Close(absl::optional<gin_helper::Dictionary> options) {
+  bool dispatch_beforeunload = false;
+  if (options)
+    options->Get("waitForBeforeUnload", &dispatch_beforeunload);
+  if (dispatch_beforeunload &&
+      web_contents()->NeedToFireBeforeUnloadOrUnloadEvents()) {
+    NotifyUserActivation();
+    web_contents()->DispatchBeforeUnload(false /* auto_cancel */);
+  } else {
+    web_contents()->Close();
+  }
+}
+
 bool WebContents::DidAddMessageToConsole(
     content::WebContents* source,
     blink::mojom::ConsoleMessageLevel level,
@@ -1199,6 +1212,8 @@ void WebContents::CloseContents(content::WebContents* source) {
 
   for (ExtendedWebContentsObserver& observer : observers_)
     observer.OnCloseContents();
+
+  Destroy();
 }
 
 void WebContents::ActivateContents(content::WebContents* source) {
@@ -3921,6 +3936,7 @@ v8::Local<v8::ObjectTemplate> WebContents::FillObjectTemplate(
   // destroyable.
   return gin_helper::ObjectTemplateBuilder(isolate, templ)
       .SetMethod("destroy", &WebContents::Destroy)
+      .SetMethod("close", &WebContents::Close)
       .SetMethod("getBackgroundThrottling",
                  &WebContents::GetBackgroundThrottling)
       .SetMethod("setBackgroundThrottling",

+ 1 - 0
shell/browser/api/electron_api_web_contents.h

@@ -152,6 +152,7 @@ class WebContents : public ExclusiveAccessContext,
   const char* GetTypeName() override;
 
   void Destroy();
+  void Close(absl::optional<gin_helper::Dictionary> options);
   base::WeakPtr<WebContents> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }
 
   bool GetBackgroundThrottling() const;

+ 77 - 0
spec/api-web-contents-spec.ts

@@ -2133,6 +2133,83 @@ describe('webContents module', () => {
     });
   });
 
+  describe('close() method', () => {
+    afterEach(closeAllWindows);
+
+    it('closes when close() is called', async () => {
+      const w = (webContents as any).create() as WebContents;
+      const destroyed = emittedOnce(w, 'destroyed');
+      w.close();
+      await destroyed;
+      expect(w.isDestroyed()).to.be.true();
+    });
+
+    it('closes when close() is called after loading a page', async () => {
+      const w = (webContents as any).create() as WebContents;
+      await w.loadURL('about:blank');
+      const destroyed = emittedOnce(w, 'destroyed');
+      w.close();
+      await destroyed;
+      expect(w.isDestroyed()).to.be.true();
+    });
+
+    it('can be GCed before loading a page', async () => {
+      const v8Util = process._linkedBinding('electron_common_v8_util');
+      let registry: FinalizationRegistry<unknown> | null = null;
+      const cleanedUp = new Promise<number>(resolve => {
+        registry = new FinalizationRegistry(resolve as any);
+      });
+      (() => {
+        const w = (webContents as any).create() as WebContents;
+        registry!.register(w, 42);
+      })();
+      const i = setInterval(() => v8Util.requestGarbageCollectionForTesting(), 100);
+      defer(() => clearInterval(i));
+      expect(await cleanedUp).to.equal(42);
+    });
+
+    it('causes its parent browserwindow to be closed', async () => {
+      const w = new BrowserWindow({ show: false });
+      await w.loadURL('about:blank');
+      const closed = emittedOnce(w, 'closed');
+      w.webContents.close();
+      await closed;
+      expect(w.isDestroyed()).to.be.true();
+    });
+
+    it('ignores beforeunload if waitForBeforeUnload not specified', async () => {
+      const w = (webContents as any).create() as WebContents;
+      await w.loadURL('about:blank');
+      await w.executeJavaScript('window.onbeforeunload = () => "hello"; null');
+      w.on('will-prevent-unload', () => { throw new Error('unexpected will-prevent-unload'); });
+      const destroyed = emittedOnce(w, 'destroyed');
+      w.close();
+      await destroyed;
+      expect(w.isDestroyed()).to.be.true();
+    });
+
+    it('runs beforeunload if waitForBeforeUnload is specified', async () => {
+      const w = (webContents as any).create() as WebContents;
+      await w.loadURL('about:blank');
+      await w.executeJavaScript('window.onbeforeunload = () => "hello"; null');
+      const willPreventUnload = emittedOnce(w, 'will-prevent-unload');
+      w.close({ waitForBeforeUnload: true });
+      await willPreventUnload;
+      expect(w.isDestroyed()).to.be.false();
+    });
+
+    it('overriding beforeunload prevention results in webcontents close', async () => {
+      const w = (webContents as any).create() as WebContents;
+      await w.loadURL('about:blank');
+      await w.executeJavaScript('window.onbeforeunload = () => "hello"; null');
+      w.once('will-prevent-unload', e => e.preventDefault());
+      const destroyed = emittedOnce(w, 'destroyed');
+      w.close({ waitForBeforeUnload: true });
+      await destroyed;
+      expect(w.isDestroyed()).to.be.true();
+    });
+  });
+
   describe('content-bounds-updated event', () => {
     afterEach(closeAllWindows);
     it('emits when moveTo is called', async () => {