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

`bringToFront` DevTools message is sent when breakpoint is triggered or inspect is called and Chromium upon this message activates DevTools via `DevToolsUIBindings::Delegate::ActivateWindow`:
```
void DevToolsWindow::ActivateWindow() {
  if (life_stage_ != kLoadCompleted)
    return;
\#if BUILDFLAG(IS_ANDROID)
  NOTIMPLEMENTED();
\#else
  if (is_docked_ && GetInspectedBrowserWindow())
    main_web_contents_->Focus();
  else if (!is_docked_ && browser_ && !browser_->window()->IsActive())
    browser_->window()->Activate();
\#endif
}
```
which implements: `DevToolsUIBindings::Delegate::ActivateWindow`.

Electron also implements this interface in:
`electron::InspectableWebContents`. However it was only setting a zoom level, therefore this commit extends it with activation of the DevTools.

Only supported for DevTools manged by `electron::InspectableWebContents`.

Closes: #37388

#### Description of Change

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
-->

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [ ] PR description included and stakeholders cc'd
- [ ] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

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', () => {