diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 0de4fd03545c5..95d5278672901 100644 --- a/docs/api/web-contents.md +++ b/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. diff --git a/shell/browser/api/electron_api_browser_window.cc b/shell/browser/api/electron_api_browser_window.cc index 5b5adb540aa6d..422512af1e957 100644 --- a/shell/browser/api/electron_api_browser_window.cc +++ b/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*) { diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index fe5380654170c..41d0408ff1f0d 100755 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -1006,6 +1006,19 @@ void WebContents::Destroy() { } } +void WebContents::Close(absl::optional 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, @@ -1196,6 +1209,8 @@ void WebContents::CloseContents(content::WebContents* source) { for (ExtendedWebContentsObserver& observer : observers_) observer.OnCloseContents(); + + Destroy(); } void WebContents::ActivateContents(content::WebContents* source) { @@ -3918,6 +3933,7 @@ v8::Local WebContents::FillObjectTemplate( // destroyable. return gin_helper::ObjectTemplateBuilder(isolate, templ) .SetMethod("destroy", &WebContents::Destroy) + .SetMethod("close", &WebContents::Close) .SetMethod("getBackgroundThrottling", &WebContents::GetBackgroundThrottling) .SetMethod("setBackgroundThrottling", diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 9cec3ff614f85..d5c59ab6fa453 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/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 options); base::WeakPtr GetWeakPtr() { return weak_factory_.GetWeakPtr(); } bool GetBackgroundThrottling() const; diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index 98251ac79035f..4be69cfb74ea4 100644 --- a/spec/api-web-contents-spec.ts +++ b/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 | null = null; + const cleanedUp = new Promise(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 () => {