Repository: electron/electron
Pull request: feat: webFrameMain.fromFrameToken

#### Description of Change

Follow up to https://github.com/electron/electron/pull/47616

Adds `webFrameMain.fromFrameToken(processId, frameToken)` which should be the defacto method of looking up frames based on the current chromium architecture.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] 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: Added `webFrameMain.fromFrameToken(processId, frameToken)` to get a `WebFrameMain` instance from its frame token.


Changed files:
filename: docs/api/web-frame-main.md
patch: @@ -66,6 +66,16 @@ These methods can be accessed from the `webFrameMain` module:
 Returns `WebFrameMain | undefined` - A frame with the given process and routing IDs,
 or `undefined` if there is no WebFrameMain associated with the given IDs.
 
+### `webFrameMain.fromFrameToken(processId, frameToken)`
+
+* `processId` Integer - An `Integer` representing the internal ID of the process which owns the frame.
+* `frameToken` string - A `string` token identifying the unique frame. Can also
+  be retrieved in the renderer process via
+  [`webFrame.frameToken`](web-frame.md#webframeframetoken-readonly).
+
+Returns `WebFrameMain | null` - A frame with the given process and frame token,
+or `null` if there is no WebFrameMain associated with the given IDs.
+
 ## Class: WebFrameMain
 
 Process: [Main](../glossary.md#main-process)<br />

filename: lib/browser/api/web-frame-main.ts
patch: @@ -1,7 +1,7 @@
 import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
 import { MessagePortMain } from '@electron/internal/browser/message-port-main';
 
-const { WebFrameMain, fromId } = process._linkedBinding('electron_browser_web_frame_main');
+const { WebFrameMain, fromId, fromFrameToken } = process._linkedBinding('electron_browser_web_frame_main');
 
 Object.defineProperty(WebFrameMain.prototype, 'ipc', {
   get () {
@@ -43,5 +43,6 @@ WebFrameMain.prototype.postMessage = function (...args) {
 };
 
 export default {
-  fromId
+  fromId,
+  fromFrameToken
 };

filename: shell/browser/api/electron_api_web_frame_main.cc
patch: @@ -641,6 +641,31 @@ v8::Local<v8::Value> FromID(gin_helper::ErrorThrower thrower,
   return WebFrameMain::From(thrower.isolate(), rfh).ToV8();
 }
 
+v8::Local<v8::Value> FromFrameToken(gin_helper::ErrorThrower thrower,
+                                    int render_process_id,
+                                    std::string render_frame_token) {
+  if (!electron::Browser::Get()->is_ready()) {
+    thrower.ThrowError("WebFrameMain is available only after app ready");
+    return v8::Null(thrower.isolate());
+  }
+
+  auto token = base::Token::FromString(render_frame_token);
+  if (!token)
+    return v8::Null(thrower.isolate());
+  auto unguessable_token =
+      base::UnguessableToken::Deserialize(token->high(), token->low());
+  if (!unguessable_token)
+    return v8::Null(thrower.isolate());
+  auto frame_token = blink::LocalFrameToken(unguessable_token.value());
+
+  auto* rfh = content::RenderFrameHost::FromFrameToken(
+      content::GlobalRenderFrameHostToken(render_process_id, frame_token));
+  if (!rfh)
+    return v8::Null(thrower.isolate());
+
+  return WebFrameMain::From(thrower.isolate(), rfh).ToV8();
+}
+
 v8::Local<v8::Value> FromIdIfExists(gin_helper::ErrorThrower thrower,
                                     int render_process_id,
                                     int render_frame_id) {
@@ -677,6 +702,7 @@ void Initialize(v8::Local<v8::Object> exports,
   gin_helper::Dictionary dict{isolate, exports};
   dict.Set("WebFrameMain", WebFrameMain::GetConstructor(isolate, context));
   dict.SetMethod("fromId", &FromID);
+  dict.SetMethod("fromFrameToken", &FromFrameToken);
   dict.SetMethod("_fromIdIfExists", &FromIdIfExists);
   dict.SetMethod("_fromFtnIdIfExists", &FromFtnIdIfExists);
 }

filename: spec/api-web-frame-main-spec.ts
patch: @@ -496,6 +496,19 @@ describe('webFrameMain module', () => {
     });
   });
 
+  describe('webFrameMain.fromFrameToken', () => {
+    it('returns null for unknown IDs', () => {
+      expect(webFrameMain.fromFrameToken(0, '')).to.be.null();
+    });
+
+    it('can find existing frame', async () => {
+      const w = new BrowserWindow({ show: false });
+      const { mainFrame } = w.webContents;
+      const frame = webFrameMain.fromFrameToken(mainFrame.processId, mainFrame.frameToken);
+      expect(frame).to.equal(mainFrame);
+    });
+  });
+
   describe('webFrameMain.collectJavaScriptCallStack', () => {
     let server: Server;
     before(async () => {

filename: typings/internal-ambient.d.ts
patch: @@ -133,7 +133,8 @@ declare namespace NodeJS {
 
   interface WebFrameMainBinding {
     WebFrameMain: typeof Electron.WebFrameMain;
-    fromId(processId: number, routingId: number): Electron.WebFrameMain;
+    fromId(processId: number, routingId: number): Electron.WebFrameMain | undefined;
+    fromFrameToken(processId: number, frameToken: string): Electron.WebFrameMain | null;
     _fromIdIfExists(processId: number, routingId: number): Electron.WebFrameMain | null;
     _fromFtnIdIfExists(frameTreeNodeId: number): Electron.WebFrameMain | null;
   }
@@ -153,6 +154,7 @@ declare namespace NodeJS {
 
   interface WebFrameBinding {
     mainFrame: InternalWebFrame;
+    WebFrame: Electron.WebFrame;
   }
 
   type DataPipe = {