diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 598c9e428d4f..f04267bc6134 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1252,7 +1252,8 @@ Captures a snapshot of the page within `rect`. Omitting `rect` will capture the Returns `Promise` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects -if the page fails to load (see [`did-fail-load`](web-contents.md#event-did-fail-load)). +if the page fails to load (see +[`did-fail-load`](web-contents.md#event-did-fail-load)). A noop rejection handler is already attached, which avoids unhandled rejection errors. If the existing page has a beforeUnload handler, [`did-fail-load`](web-contents.md#event-did-fail-load) will be called unless [`will-prevent-unload`](web-contents.md#event-did-fail-load) is handled. Same as [`webContents.loadURL(url[, options])`](web-contents.md#contentsloadurlurl-options). diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index a163401d25fb..25fd0991c341 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1079,7 +1079,7 @@ Emitted when the [mainFrame](web-contents.md#contentsmainframe-readonly), an `` - the promise will resolve when the page has finished loading (see [`did-finish-load`](web-contents.md#event-did-finish-load)), and rejects if the page fails to load (see -[`did-fail-load`](web-contents.md#event-did-fail-load)). A noop rejection handler is already attached, which avoids unhandled rejection errors. +[`did-fail-load`](web-contents.md#event-did-fail-load)). A noop rejection handler is already attached, which avoids unhandled rejection errors. If the existing page has a beforeUnload handler, [`did-fail-load`](web-contents.md#event-did-fail-load) will be called unless [`will-prevent-unload`](web-contents.md#event-did-fail-load) is handled. Loads the `url` in the window. The `url` must contain the protocol prefix, e.g. the `http://` or `file://`. If the load should bypass http cache then diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index f031d6f40dc9..b9d97cfff3af 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -2395,6 +2395,9 @@ void WebContents::LoadURL(const GURL& url, return; } + if (web_contents()->NeedToFireBeforeUnloadOrUnloadEvents()) + pending_unload_url_ = url; + // Discard non-committed entries to ensure we don't re-use a pending entry. web_contents()->GetController().DiscardNonCommittedEntries(); web_contents()->GetController().LoadURLWithParams(params); @@ -3897,8 +3900,15 @@ void WebContents::RunBeforeUnloadDialog(content::WebContents* web_contents, content::RenderFrameHost* rfh, bool is_reload, DialogClosedCallback callback) { - // TODO: asyncify? bool default_prevented = Emit("will-prevent-unload"); + + if (pending_unload_url_.has_value() && !default_prevented) { + Emit("did-fail-load", static_cast(net::ERR_ABORTED), + net::ErrorToShortString(net::ERR_ABORTED), + pending_unload_url_.value().possibly_invalid_spec(), true); + pending_unload_url_.reset(); + } + std::move(callback).Run(default_prevented, std::u16string()); } diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 7291cdd1798e..825d2f96264c 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -844,6 +844,8 @@ class WebContents final : public ExclusiveAccessContext, // that field to ensure the dtor destroys them in the right order. raw_ptr zoom_controller_ = nullptr; + std::optional pending_unload_url_ = std::nullopt; + // Maps url to file path, used by the file requests sent from devtools. typedef std::map PathsMap; PathsMap saved_files_; diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index 50cf54473616..215beaca70fd 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/api-web-contents-spec.ts @@ -109,6 +109,7 @@ describe('webContents module', () => { await closeAllWindows(); await cleanupWebContents(); }); + it('does not emit if beforeunload returns undefined in a BrowserWindow', async () => { const w = new BrowserWindow({ show: false }); w.webContents.once('will-prevent-unload', () => { @@ -162,6 +163,35 @@ describe('webContents module', () => { w.close(); await wait; }); + + it('fails loading a subsequent page after beforeunload is not prevented', async () => { + const w = new BrowserWindow({ show: false }); + + const didFailLoad = once(w.webContents, 'did-fail-load'); + await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'beforeunload-false.html')); + await w.webContents.executeJavaScript('console.log(\'gesture\')', true); + + w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'a.html')); + const [, code, , validatedURL] = await didFailLoad; + expect(code).to.equal(-3); // ERR_ABORTED + const { href: expectedURL } = url.pathToFileURL(path.join(__dirname, 'fixtures', 'pages', 'a.html')); + expect(validatedURL).to.equal(expectedURL); + }); + + it('allows loading a subsequent page after beforeunload is prevented', async () => { + const w = new BrowserWindow({ show: false }); + w.webContents.once('will-prevent-unload', event => event.preventDefault()); + + await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'beforeunload-false.html')); + await w.webContents.executeJavaScript('console.log(\'gesture\')', true); + await w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'a.html')); + const pageTitle = await w.webContents.executeJavaScript('document.title'); + expect(pageTitle).to.equal('test'); + + const wait = once(w, 'closed'); + w.close(); + await wait; + }); }); describe('webContents.send(channel, args...)', () => {