Repository: electron/electron (language: cpp)

Issue:
Add will-frame-navigate event
- Electron version:
## \* Operating system: window 7

Found in webContents listening 'will - navigate' with 'event callback function event. The preventDefault ();'can't stop jumping events in the iframe.Example: in the iframe "< a href =" github.com "> test < / a >"



Discussion / hints:
~~Closing as duplicate of #1378~~

I am not sure if this is a bug or enhancement, the original event was never specified for sub-frame navigations. Will defer to others for thoughts.

@deepak1556 Should we just expose the `isMainFrame` property on the event like we do on things like `did-navigate-in-page` so used can filter out iframe events if they want to?

@deepak1556 @MarshallOfSound @kevinsawicki  `did-navigate-in-page` Don't listen to change the url in the iframe, tested

@bdxing  `did-navigate-in-page` definitely fires in iFrames as I fixed an issue related to it #6808

`did-navigate-in-page` only emits for in-page navigations, that's probably why it doesn't emit for you.

We need to add a new `will-frame-navigate` event that also emits for subframe navigations, changing the behavior of `will-navigate` is not an option, since most code using the event assumes it only emits for the main frame.

@zcbenz OK, The first thank you, waiting for the event.

Hi, I would be interested in working on this issue if nobody is actively working on it?
Wow.. I actually didn't realize this was missing and probably opened up a security hole if someone really wanted to exploit it.  If you assume you're not allowing iframes to load by preventing them then you could accidentally load a page when you intended to abort.
cc @samuelmaddock 
This functionality should be easy enough to add after #29290 is merged.

Fix patch:
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index fc6edfedd467a..0301458c4dd60 100644
--- a/docs/api/web-contents.md
+++ b/docs/api/web-contents.md
@@ -19,6 +19,36 @@ const contents = win.webContents
 console.log(contents)
 ```
 
+## Navigation Events
+
+Several events can be used to monitor navigations as they occur within a `webContents`.
+
+### Document Navigations
+
+When a `webContents` navigates to another page (as opposed to an [in-page navigation](web-contents.md#in-page-navigation)), the following events will be fired.
+
+* [`did-start-navigation`](web-contents.md#event-did-start-navigation)
+* [`will-frame-navigate`](web-contents.md#event-will-frame-navigate)
+* [`will-navigate`](web-contents.md#event-will-navigate) (only fired when main frame navigates)
+* [`will-redirect`](web-contents.md#event-will-redirect) (only fired when a redirect happens during navigation)
+* [`did-redirect-navigation`](web-contents.md#event-did-redirect-navigation) (only fired when a redirect happens during navigation)
+* [`did-frame-navigate`](web-contents.md#event-did-frame-navigate)
+* [`did-navigate`](web-contents.md#event-did-navigate) (only fired when main frame navigates)
+
+Subsequent events will not fire if `event.preventDefault()` is called on any of the cancellable events.
+
+### In-page Navigation
+
+In-page navigations don't cause the page to reload, but instead navigate to a location within the current page. These events are not cancellable. For an in-page navigations, the following events will fire in this order:
+
+* [`did-start-navigation`](web-contents.md#event-did-start-navigation)
+* [`did-navigate-in-page`](web-contents.md#event-did-navigate-in-page)
+
+### Frame Navigation
+
+The [`will-navigate`](web-contents.md#event-will-navigate) and [`did-navigate`](web-contents.md#event-did-navigate) events only fire when the [mainFrame](web-contents.md#contentsmainframe-readonly) navigates.
+If you want to also observe navigations in `<iframe>`s, use [`will-frame-navigate`](web-contents.md#event-will-frame-navigate) and [`did-frame-navigate`](web-contents.md#event-did-frame-navigate) events.
+
 ## Methods
 
 These methods can be accessed from the `webContents` module:
@@ -225,9 +255,37 @@ Returns:
 * `frameProcessId` Integer _Deprecated_
 * `frameRoutingId` Integer _Deprecated_
 
-Emitted when a user or the page wants to start navigation. It can happen when
+Emitted when a user or the page wants to start navigation on the main frame. It can happen when
+the `window.location` object is changed or a user clicks a link in the page.
+
+This event will not emit when the navigation is started programmatically with
+APIs like `webContents.loadURL` and `webContents.back`.
+
+It is also not emitted for in-page navigations, such as clicking anchor links
+or updating the `window.location.hash`. Use `did-navigate-in-page` event for
+this purpose.
+
+Calling `event.preventDefault()` will prevent the navigation.
+
+#### Event: 'will-frame-navigate'
+
+Returns:
+
+* `details` Event<>
+  * `url` string - The URL the frame is navigating to.
+  * `isMainFrame` boolean - True if the navigation is taking place in a main frame.
+  * `frame` WebFrameMain - The frame to be navigated.
+  * `initiator` WebFrameMain (optional) - The frame which initiated the
+    navigation, which can be a parent frame (e.g. via `window.open` with a
+    frame's name), or null if the navigation was not initiated by a frame. This
+    can also be null if the initiating frame was deleted before the event was
+    emitted.
+
+Emitted when a user or the page wants to start navigation in any frame. It can happen when
 the `window.location` object is changed or a user clicks a link in the page.
 
+Unlike `will-navigate`, `will-frame-navigate` is fired when the main frame or any of its subframes attempts to navigate. When the navigation event comes from the main frame, `isMainFrame` will be `true`.
+
 This event will not emit when the navigation is started programmatically with
 APIs like `webContents.loadURL` and `webContents.back`.
 
diff --git a/docs/api/webview-tag.md b/docs/api/webview-tag.md
index c9c82f7832d23..f38cb9a54c793 100644
--- a/docs/api/webview-tag.md
+++ b/docs/api/webview-tag.md
@@ -823,6 +823,28 @@ this purpose.
 
 Calling `event.preventDefault()` does __NOT__ have any effect.
 
+### Event: 'will-frame-navigate'
+
+Returns:
+
+* `url` string
+* `isMainFrame` boolean
+* `frameProcessId` Integer
+* `frameRoutingId` Integer
+
+Emitted when a user or the page wants to start navigation anywhere in the `<webview>`
+or any frames embedded within. It can happen when the `window.location` object is
+changed or a user clicks a link in the page.
+
+This event will not emit when the navigation is started programmatically with
+APIs like `<webview>.loadURL` and `<webview>.back`.
+
+It is also not emitted during in-page navigation, such as clicking anchor links
+or updating the `window.location.hash`. Use `did-navigate-in-page` event for
+this purpose.
+
+Calling `event.preventDefault()` does __NOT__ have any effect.
+
 ### Event: 'did-start-navigation'
 
 Returns:
diff --git a/lib/browser/guest-view-manager.ts b/lib/browser/guest-view-manager.ts
index 322b82aaec4bc..a43c9cacfbd62 100644
--- a/lib/browser/guest-view-manager.ts
+++ b/lib/browser/guest-view-manager.ts
@@ -163,6 +163,16 @@ const createGuest = function (embedder: Electron.WebContents, embedderFrameId: n
     });
   });
 
+  // Dispatch guest's frame navigation event to embedder.
+  guest.on('will-frame-navigate', function (event: Electron.WebContentsWillFrameNavigateEventParams) {
+    sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'will-frame-navigate', {
+      url: event.url,
+      isMainFrame: event.isMainFrame,
+      frameProcessId: event.frame.processId,
+      frameRoutingId: event.frame.routingId
+    });
+  });
+
   // Notify guest of embedder window visibility when it is ready
   // FIXME Remove once https://github.com/electron/electron/issues/6828 is fixed
   guest.on('dom-ready', function () {
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index 111879fd7cd85..878116bcfae90 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -623,6 +623,23 @@ void SetBackgroundColor(content::RenderWidgetHostView* rwhv, SkColor color) {
       ->SetContentBackgroundColor(color);
 }
 
+content::RenderFrameHost* GetRenderFrameHost(
+    content::NavigationHandle* navigation_handle) {
+  int frame_tree_node_id = navigation_handle->GetFrameTreeNodeId();
+  content::FrameTreeNode* frame_tree_node =
+      content::FrameTreeNode::GloballyFindByID(frame_tree_node_id);
+  content::RenderFrameHostManager* render_manager =
+      frame_tree_node->render_manager();
+  content::RenderFrameHost* frame_host = nullptr;
+  if (render_manager) {
+    frame_host = render_manager->speculative_frame_host();
+    if (!frame_host)
+      frame_host = render_manager->current_frame_host();
+  }
+
+  return frame_host;
+}
+
 }  // namespace
 
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
@@ -1769,18 +1786,8 @@ bool WebContents::EmitNavigationEvent(
     const std::string& event_name,
     content::NavigationHandle* navigation_handle) {
   bool is_main_frame = navigation_handle->IsInMainFrame();
-  int frame_tree_node_id = navigation_handle->GetFrameTreeNodeId();
-  content::FrameTreeNode* frame_tree_node =
-      content::FrameTreeNode::GloballyFindByID(frame_tree_node_id);
-  content::RenderFrameHostManager* render_manager =
-      frame_tree_node->render_manager();
-  content::RenderFrameHost* frame_host = nullptr;
-  if (render_manager) {
-    frame_host = render_manager->speculative_frame_host();
-    if (!frame_host)
-      frame_host = render_manager->current_frame_host();
-  }
   int frame_process_id = -1, frame_routing_id = -1;
+  content::RenderFrameHost* frame_host = GetRenderFrameHost(navigation_handle);
   if (frame_host) {
     frame_process_id = frame_host->GetProcess()->GetID();
     frame_routing_id = frame_host->GetRoutingID();
diff --git a/shell/browser/electron_navigation_throttle.cc b/shell/browser/electron_navigation_throttle.cc
index 8397f8bfd9616..e5c66077c0261 100644
--- a/shell/browser/electron_navigation_throttle.cc
+++ b/shell/browser/electron_navigation_throttle.cc
@@ -37,6 +37,10 @@ ElectronNavigationThrottle::WillStartRequest() {
     return PROCEED;
   }
 
+  if (handle->IsRendererInitiated() &&
+      api_contents->EmitNavigationEvent("will-frame-navigate", handle)) {
+    return CANCEL;
+  }
   if (handle->IsRendererInitiated() && handle->IsInMainFrame() &&
       api_contents->EmitNavigationEvent("will-navigate", handle)) {
     return CANCEL;
diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts
index 298379a9fd64a..1c119af0dcd1f 100644
--- a/spec/api-browser-window-spec.ts
+++ b/spec/api-browser-window-spec.ts
@@ -6,7 +6,7 @@ import * as qs from 'querystring';
 import * as http from 'http';
 import * as os from 'os';
 import { AddressInfo } from 'net';
-import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, WebFrameMain } from 'electron/main';
+import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, webFrameMain, session, WebContents, WebFrameMain } from 'electron/main';
 
 import { emittedUntil, emittedNTimes } from './lib/events-helpers';
 import { ifit, ifdescribe, defer, listen } from './lib/spec-helpers';
@@ -568,6 +568,205 @@ describe('BrowserWindow module', () => {
         });
       });
 
+      describe('will-frame-navigate event', () => {
+        let server = null as unknown as http.Server;
+        let url = null as unknown as string;
+        before((done) => {
+          server = http.createServer((req, res) => {
+            if (req.url === '/navigate-top') {
+              res.end('<a target=_top href="/">navigate _top</a>');
+            } else if (req.url === '/navigate-iframe') {
+              res.end('<a href="/test">navigate iframe</a>');
+            } else if (req.url === '/navigate-iframe?navigated') {
+              res.end('Successfully navigated');
+            } else if (req.url === '/navigate-iframe-immediately') {
+              res.end(`
+                <script type="text/javascript" charset="utf-8">
+                  location.href += '?navigated'
+                </script>
+              `);
+            } else if (req.url === '/navigate-iframe-immediately?navigated') {
+              res.end('Successfully navigated');
+            } else {
+              res.end('');
+            }
+          });
+          server.listen(0, '127.0.0.1', () => {
+            url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
+            done();
+          });
+        });
+
+        after(() => {
+          server.close();
+        });
+
+        it('allows the window to be closed from the event listener', (done) => {
+          w.webContents.once('will-frame-navigate', () => {
+            w.close();
+            done();
+          });
+          w.loadFile(path.join(fixtures, 'pages', 'will-navigate.html'));
+        });
+
+        it('can be prevented', (done) => {
+          let willNavigate = false;
+          w.webContents.once('will-frame-navigate', (e) => {
+            willNavigate = true;
+            e.preventDefault();
+          });
+          w.webContents.on('did-stop-loading', () => {
+            if (willNavigate) {
+              // i.e. it shouldn't have had '?navigated' appended to it.
+              try {
+                expect(w.webContents.getURL().endsWith('will-navigate.html')).to.be.true();
+                done();
+              } catch (e) {
+                done(e);
+              }
+            }
+          });
+          w.loadFile(path.join(fixtures, 'pages', 'will-navigate.html'));
+        });
+
+        it('can be prevented when navigating subframe', (done) => {
+          let willNavigate = false;
+          w.webContents.on('did-frame-navigate', (_event, _url, _httpResponseCode, _httpStatusText, isMainFrame, frameProcessId, frameRoutingId) => {
+            if (isMainFrame) return;
+
+            w.webContents.once('will-frame-navigate', (e) => {
+              willNavigate = true;
+              e.preventDefault();
+            });
+
+            w.webContents.on('did-stop-loading', () => {
+              const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
+              expect(frame).to.not.be.undefined();
+              if (willNavigate) {
+                // i.e. it shouldn't have had '?navigated' appended to it.
+                try {
+                  expect(frame!.url.endsWith('/navigate-iframe-immediately')).to.be.true();
+                  done();
+                } catch (e) {
+                  done(e);
+                }
+              }
+            });
+          });
+          w.loadURL(`data:text/html,<iframe src="http://127.0.0.1:${(server.address() as AddressInfo).port}/navigate-iframe-immediately"></iframe>`);
+        });
+
+        it('is triggered when navigating from file: to http:', async () => {
+          await w.loadFile(path.join(fixtures, 'api', 'blank.html'));
+          w.webContents.executeJavaScript(`location.href = ${JSON.stringify(url)}`);
+          const navigatedTo = await new Promise(resolve => {
+            w.webContents.once('will-frame-navigate', (e) => {
+              e.preventDefault();
+              resolve(e.url);
+            });
+          });
+          expect(navigatedTo).to.equal(url);
+          expect(w.webContents.getURL()).to.match(/^file:/);
+        });
+
+        it('is triggered when navigating from about:blank to http:', async () => {
+          await w.loadURL('about:blank');
+          w.webContents.executeJavaScript(`location.href = ${JSON.stringify(url)}`);
+          const navigatedTo = await new Promise(resolve => {
+            w.webContents.once('will-frame-navigate', (e) => {
+              e.preventDefault();
+              resolve(e.url);
+            });
+          });
+          expect(navigatedTo).to.equal(url);
+          expect(w.webContents.getURL()).to.equal('about:blank');
+        });
+
+        it('is triggered when a cross-origin iframe navigates _top', async () => {
+          await w.loadURL(`data:text/html,<iframe src="http://127.0.0.1:${(server.address() as AddressInfo).port}/navigate-top"></iframe>`);
+          await setTimeout(1000);
+
+          let willFrameNavigateEmitted = false;
+          let isMainFrameValue;
+          w.webContents.on('will-frame-navigate', (event) => {
+            willFrameNavigateEmitted = true;
+            isMainFrameValue = event.isMainFrame;
+          });
+          const didNavigatePromise = once(w.webContents, 'did-navigate');
+
+          w.webContents.debugger.attach('1.1');
+          const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
+          const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe');
+          const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
+            targetId: iframeTarget.targetId,
+            flatten: true
+          });
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mousePressed',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mouseReleased',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+
+          await didNavigatePromise;
+
+          expect(willFrameNavigateEmitted).to.be.true();
+          expect(isMainFrameValue).to.be.true();
+        });
+
+        it('is triggered when a cross-origin iframe navigates itself', async () => {
+          await w.loadURL(`data:text/html,<iframe src="http://127.0.0.1:${(server.address() as AddressInfo).port}/navigate-iframe"></iframe>`);
+          await setTimeout(1000);
+
+          let willNavigateEmitted = false;
+          let isMainFrameValue;
+          w.webContents.on('will-frame-navigate', (event) => {
+            willNavigateEmitted = true;
+            isMainFrameValue = event.isMainFrame;
+          });
+          const didNavigatePromise = once(w.webContents, 'did-frame-navigate');
+
+          w.webContents.debugger.attach('1.1');
+          const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
+          const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe');
+          const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
+            targetId: iframeTarget.targetId,
+            flatten: true
+          });
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mousePressed',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mouseReleased',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+
+          await didNavigatePromise;
+
+          expect(willNavigateEmitted).to.be.true();
+          expect(isMainFrameValue).to.be.false();
+        });
+
+        it('can cancel when a cross-origin iframe navigates itself', async () => {
+
+        });
+      });
+
       describe('will-redirect event', () => {
         let server: http.Server;
         let url: string;
@@ -652,6 +851,179 @@ describe('BrowserWindow module', () => {
           w.loadURL(`${url}/navigate-302`);
         });
       });
+
+      describe('ordering', () => {
+        let server = null as unknown as http.Server;
+        let url = null as unknown as string;
+        const navigationEvents = [
+          'did-start-navigation',
+          'did-navigate-in-page',
+          'will-frame-navigate',
+          'will-navigate',
+          'will-redirect',
+          'did-redirect-navigation',
+          'did-frame-navigate',
+          'did-navigate'
+        ];
+        before((done) => {
+          server = http.createServer((req, res) => {
+            if (req.url === '/navigate') {
+              res.end('<a href="/">navigate</a>');
+            } else if (req.url === '/redirect') {
+              res.end('<a href="/redirect2">redirect</a>');
+            } else if (req.url === '/redirect2') {
+              res.statusCode = 302;
+              res.setHeader('location', url);
+              res.end();
+            } else if (req.url === '/in-page') {
+              res.end('<a href="#in-page">redirect</a><div id="in-page"></div>');
+            } else {
+              res.end('');
+            }
+          });
+          server.listen(0, '127.0.0.1', () => {
+            url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
+            done();
+          });
+        });
+        it('for initial navigation, event order is consistent', async () => {
+          const firedEvents: string[] = [];
+          const expectedEventOrder = [
+            'did-start-navigation',
+            'did-frame-navigate',
+            'did-navigate'
+          ];
+          const allEvents = Promise.all(navigationEvents.map(event =>
+            once(w.webContents, event).then(() => firedEvents.push(event))
+          ));
+          const timeout = setTimeout(1000);
+          w.loadURL(url);
+          await Promise.race([allEvents, timeout]);
+          expect(firedEvents).to.deep.equal(expectedEventOrder);
+        });
+
+        it('for second navigation, event order is consistent', async () => {
+          const firedEvents: string[] = [];
+          const expectedEventOrder = [
+            'did-start-navigation',
+            'will-frame-navigate',
+            'will-navigate',
+            'did-frame-navigate',
+            'did-navigate'
+          ];
+          w.loadURL(`${url}navigate`);
+          await once(w.webContents, 'did-navigate');
+          await setTimeout(1000);
+          navigationEvents.forEach(event =>
+            once(w.webContents, event).then(() => firedEvents.push(event))
+          );
+          const navigationFinished = once(w.webContents, 'did-navigate');
+          w.webContents.debugger.attach('1.1');
+          const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
+          const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page');
+          const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
+            targetId: pageTarget.targetId,
+            flatten: true
+          });
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mousePressed',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mouseReleased',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await navigationFinished;
+          expect(firedEvents).to.deep.equal(expectedEventOrder);
+        });
+
+        it('when navigating with redirection, event order is consistent', async () => {
+          const firedEvents: string[] = [];
+          const expectedEventOrder = [
+            'did-start-navigation',
+            'will-frame-navigate',
+            'will-navigate',
+            'will-redirect',
+            'did-redirect-navigation',
+            'did-frame-navigate',
+            'did-navigate'
+          ];
+          w.loadURL(`${url}redirect`);
+          await once(w.webContents, 'did-navigate');
+          await setTimeout(1000);
+          navigationEvents.forEach(event =>
+            once(w.webContents, event).then(() => firedEvents.push(event))
+          );
+          const navigationFinished = once(w.webContents, 'did-navigate');
+          w.webContents.debugger.attach('1.1');
+          const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
+          const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page');
+          const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
+            targetId: pageTarget.targetId,
+            flatten: true
+          });
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mousePressed',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mouseReleased',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await navigationFinished;
+          expect(firedEvents).to.deep.equal(expectedEventOrder);
+        });
+
+        it('when navigating in-page, event order is consistent', async () => {
+          const firedEvents: string[] = [];
+          const expectedEventOrder = [
+            'did-start-navigation',
+            'did-navigate-in-page'
+          ];
+          w.loadURL(`${url}in-page`);
+          await once(w.webContents, 'did-navigate');
+          await setTimeout(1000);
+          navigationEvents.forEach(event =>
+            once(w.webContents, event).then(() => firedEvents.push(event))
+          );
+          const navigationFinished = once(w.webContents, 'did-navigate-in-page');
+          w.webContents.debugger.attach('1.1');
+          const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
+          const pageTarget = targets.targetInfos.find((t: any) => t.type === 'page');
+          const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
+            targetId: pageTarget.targetId,
+            flatten: true
+          });
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mousePressed',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
+            type: 'mouseReleased',
+            x: 10,
+            y: 10,
+            clickCount: 1,
+            button: 'left'
+          }, sessionId);
+          await navigationFinished;
+          expect(firedEvents).to.deep.equal(expectedEventOrder);
+        });
+      });
     });
   }
 
diff --git a/spec/fixtures/pages/webview-will-navigate-in-frame.html b/spec/fixtures/pages/webview-will-navigate-in-frame.html
new file mode 100644
index 0000000000000..63f921e724278
--- /dev/null
+++ b/spec/fixtures/pages/webview-will-navigate-in-frame.html
@@ -0,0 +1,12 @@
+<html>
+<body>
+  <script>
+    function loadSubframe() {
+      const frame = document.createElement("iframe");
+      frame.id = "frame";
+      frame.setAttribute("src", "./webview-will-navigate.html");
+      document.body.appendChild(frame);
+    }
+  </script>
+</body>
+</html>
diff --git a/spec/webview-spec.ts b/spec/webview-spec.ts
index c6a4d514d91e8..8d0f78a5c8003 100644
--- a/spec/webview-spec.ts
+++ b/spec/webview-spec.ts
@@ -1480,7 +1480,7 @@ describe('<webview> tag', function () {
     });
 
     describe('will-navigate event', () => {
-      it('emits when a url that leads to outside of the page is clicked', async () => {
+      it('emits when a url that leads to outside of the page is loaded', async () => {
         const { url } = await loadWebViewAndWaitForEvent(w, {
           src: `file://${fixtures}/pages/webview-will-navigate.html`
         }, 'will-navigate');
@@ -1489,6 +1489,47 @@ describe('<webview> tag', function () {
       });
     });
 
+    describe('will-frame-navigate event', () => {
+      it('emits when a link that leads to outside of the page is loaded', async () => {
+        const { url, isMainFrame } = await loadWebViewAndWaitForEvent(w, {
+          src: `file://${fixtures}/pages/webview-will-navigate.html`
+        }, 'will-frame-navigate');
+        expect(url).to.equal('http://host/');
+        expect(isMainFrame).to.be.true();
+      });
+
+      it('emits when a link within an iframe, which leads to outside of the page, is loaded', async () => {
+        await loadWebView(w, {
+          src: `file://${fixtures}/pages/webview-will-navigate-in-frame.html`,
+          nodeIntegration: ''
+        });
+
+        const { url, frameProcessId, frameRoutingId } = await w.executeJavaScript(`
+          new Promise((resolve, reject) => {
+            let hasFrameNavigatedOnce = false;
+            const webview = document.getElementById('webview');
+            webview.addEventListener('will-frame-navigate', ({url, isMainFrame, frameProcessId, frameRoutingId}) => {
+              if (isMainFrame) return;
+              if (hasFrameNavigatedOnce) resolve({
+                url,
+                isMainFrame,
+                frameProcessId,
+                frameRoutingId,
+              });
+
+              // First navigation is the initial iframe load within the <webview>
+              hasFrameNavigatedOnce = true;
+            });
+            webview.executeJavaScript('loadSubframe()');
+          });
+        `);
+
+        expect(url).to.equal('http://host/');
+        expect(frameProcessId).to.be.a('number');
+        expect(frameRoutingId).to.be.a('number');
+      });
+    });
+
     describe('did-navigate event', () => {
       it('emits when a url that leads to outside of the page is clicked', async () => {
         const pageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-will-navigate.html')).toString();


Test patch: