Repository: electron/electron
Pull request: feat: Focus DevTools when breakpoint is triggered

Backport of #46386

See that PR for details.


Notes: Automatically focus DevTools when element is inspected or breakpoint is triggered. <!-- Please add a one-line description for app developers to read in the release notes, or 'none' if no notes relevant to app developers. Examples and help on special cases: https://github.com/electron/clerk/blob/main/README.md#examples -->


Changed files:
filename: shell/browser/ui/inspectable_web_contents.cc
patch: @@ -520,6 +520,12 @@ void InspectableWebContents::UpdateDevToolsZoomLevel(double level) {
 }
 
 void InspectableWebContents::ActivateWindow() {
+  if (embedder_message_dispatcher_) {
+    if (managed_devtools_web_contents_ && view_) {
+      view_->ActivateDevTools();
+    }
+  }
+
   // Set the zoom level.
   SetZoomLevelForWebContents(GetDevToolsWebContents(), GetDevToolsZoomLevel());
 }

filename: shell/browser/ui/inspectable_web_contents_view.cc
patch: @@ -132,6 +132,23 @@ void InspectableWebContentsView::ShowDevTools(bool activate) {
   }
 }
 
+void InspectableWebContentsView::ActivateDevTools() {
+  if (!devtools_visible_) {
+    return;
+  }
+  if (devtools_window_) {
+    if (!devtools_window_->IsActive()) {
+      devtools_window_->Activate();
+    }
+    return;
+  }
+  if (devtools_web_view_) {
+    if (!devtools_web_view_->HasFocus()) {
+      devtools_web_view_->RequestFocus();
+    }
+  }
+}
+
 void InspectableWebContentsView::CloseDevTools() {
   if (!devtools_visible_)
     return;

filename: shell/browser/ui/inspectable_web_contents_view.h
patch: @@ -49,6 +49,7 @@ class InspectableWebContentsView : public views::View {
   void SetCornerRadii(const gfx::RoundedCornersF& corner_radii);
 
   void ShowDevTools(bool activate);
+  void ActivateDevTools();
   void CloseDevTools();
   bool IsDevToolsViewShowing();
   bool IsDevToolsViewFocused();

filename: spec/api-web-contents-spec.ts
patch: @@ -1,6 +1,6 @@
 import { BrowserWindow, ipcMain, webContents, session, app, BrowserView, WebContents, BaseWindow, WebContentsView } from 'electron/main';
 
-import { expect } from 'chai';
+import { assert, expect } from 'chai';
 
 import * as cp from 'node:child_process';
 import { once } from 'node:events';
@@ -1013,6 +1013,41 @@ describe('webContents module', () => {
       await devToolsClosed;
       expect(() => { webContents.getFocusedWebContents(); }).to.not.throw();
     });
+
+    it('Inspect activates detached devtools window', async () => {
+      const window = new BrowserWindow({ show: true });
+      await window.loadURL('about:blank');
+      const webContentsBeforeOpenedDevtools = webContents.getAllWebContents();
+
+      const windowWasBlurred = once(window, 'blur');
+      window.webContents.openDevTools({ mode: 'detach' });
+      await windowWasBlurred;
+
+      let devToolsWebContents = null;
+      for (const newWebContents of webContents.getAllWebContents()) {
+        const oldWebContents = webContentsBeforeOpenedDevtools.find(
+          oldWebContents => {
+            return newWebContents.id === oldWebContents.id;
+          });
+        if (oldWebContents !== null) {
+          devToolsWebContents = newWebContents;
+          break;
+        }
+      }
+      assert(devToolsWebContents !== null);
+
+      const windowFocused = once(window, 'focus');
+      window.focus();
+      await windowFocused;
+
+      expect(devToolsWebContents.isFocused()).to.be.false();
+      const devToolsWebContentsFocused = once(devToolsWebContents, 'focus');
+      window.webContents.inspectElement(100, 100);
+      await devToolsWebContentsFocused;
+
+      expect(devToolsWebContents.isFocused()).to.be.true();
+      expect(window.isFocused()).to.be.false();
+    });
   });
 
   describe('setDevToolsWebContents() API', () => {