From f88564b56dfb2cee565cc6d4dcd51d2432b776d1 Mon Sep 17 00:00:00 2001 From: Ppsoft1991 <18442117+Ppsoft1991@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:55:13 +0800 Subject: [PATCH] cdp: expose child frame metadata Include parentId on child frame navigation events and recursively serialize child frames in Page.getFrameTree using their live frame and loader IDs. Fixes #3014 --- src/browser/tests/cdp/empty_iframe.html | 1 + src/cdp/domains/page.zig | 108 +++++++++++++++++++----- 2 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 src/browser/tests/cdp/empty_iframe.html diff --git a/src/browser/tests/cdp/empty_iframe.html b/src/browser/tests/cdp/empty_iframe.html new file mode 100644 index 000000000..e2acf8b4a --- /dev/null +++ b/src/browser/tests/cdp/empty_iframe.html @@ -0,0 +1 @@ +main diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index c718b576d..199ca1179 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -77,6 +77,7 @@ pub fn processMessage(cmd: *CDP.Command) !void { const CDPFrame = struct { id: []const u8, + parentId: ?[]const u8 = null, loaderId: []const u8, url: []const u8, domainAndRegistry: []const u8 = "", @@ -90,6 +91,44 @@ const CDPFrame = struct { gatedAPIFeatures: [][]const u8 = &[0][]const u8{}, }; +const CDPFrameTree = struct { + frame: CDPFrame, + childFrames: ?[]CDPFrameTree = null, +}; + +fn serializeFrame(arena: Allocator, bc: *const CDP.BrowserContext, frame: *const Frame) !CDPFrame { + const frame_id = id.toFrameId(frame._frame_id); + const loader_id = id.toLoaderId(frame._loader_id); + const parent_id = if (frame.parent) |parent| blk: { + const value = id.toFrameId(parent._frame_id); + break :blk try arena.dupe(u8, &value); + } else null; + + return .{ + .id = try arena.dupe(u8, &frame_id), + .parentId = parent_id, + .loaderId = try arena.dupe(u8, &loader_id), + .url = frame.url, + .securityOrigin = frame.origin orelse bc.security_origin, + .secureContextType = bc.secure_context_type, + }; +} + +fn serializeFrameTree(arena: Allocator, bc: *const CDP.BrowserContext, frame: *const Frame) !CDPFrameTree { + const child_frames = frame.child_frames.items; + const children = if (child_frames.len == 0) null else try arena.alloc(CDPFrameTree, child_frames.len); + if (children) |items| { + for (child_frames, items) |child, *serialized| { + serialized.* = try serializeFrameTree(arena, bc, child); + } + } + + return .{ + .frame = try serializeFrame(arena, bc, frame), + .childFrames = children, + }; +} + fn getFrameTree(cmd: *CDP.Command) !void { // Stagehand parses the response and error if we don't return a // correct one for this call when browser context or target id are missing. @@ -105,18 +144,11 @@ fn getFrameTree(cmd: *CDP.Command) !void { }, }; const bc = cmd.browser_context orelse return cmd.sendResult(startup, .{}); - const target_id = bc.target_id orelse return cmd.sendResult(startup, .{}); + _ = bc.target_id orelse return cmd.sendResult(startup, .{}); + const frame = bc.mainFrame() orelse return cmd.sendResult(startup, .{}); return cmd.sendResult(.{ - .frameTree = .{ - .frame = CDPFrame{ - .id = &target_id, - .securityOrigin = bc.security_origin, - .loaderId = "LID-0000000001", - .url = bc.getURL() orelse "about:blank", - .secureContextType = bc.secure_context_type, - }, - }, + .frameTree = try serializeFrameTree(cmd.arena, bc, frame), }, .{}); } @@ -681,13 +713,7 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N // frameNavigated event try cdp.sendEvent("Page.frameNavigated", .{ .type = "Navigation", - .frame = CDPFrame{ - .id = frame_id, - .url = event.url, - .loaderId = loader_id, - .securityOrigin = bc.security_origin, - .secureContextType = bc.secure_context_type, - }, + .frame = try serializeFrame(arena, bc, frame), }, .{ .session_id = session_id }); { @@ -1027,16 +1053,18 @@ test "cdp.frame: getFrameTree" { } const bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* }); + const root = bc.mainFrame() orelse unreachable; + const root_id = id.toFrameId(root._frame_id); { try ctx.processMessage(.{ .id = 11, .method = "Page.getFrameTree" }); try ctx.expectSentResult(.{ .frameTree = .{ .frame = .{ - .id = "FID-000000000X", + .id = &root_id, .loaderId = "LID-0000000001", .url = "http://127.0.0.1:9582/src/browser/tests/hi.html", .domainAndRegistry = "", - .securityOrigin = bc.security_origin, + .securityOrigin = root.origin orelse bc.security_origin, .mimeType = "text/html", .adFrameStatus = .{ .adFrameType = "none", @@ -1055,11 +1083,11 @@ test "cdp.frame: getFrameTree" { try ctx.expectSentResult(.{ .frameTree = .{ .frame = .{ - .id = "FID-000000000X", + .id = &root_id, .loaderId = "LID-0000000001", .url = "http://127.0.0.1:9582/src/browser/tests/hi.html", .domainAndRegistry = "", - .securityOrigin = bc.security_origin, + .securityOrigin = root.origin orelse bc.security_origin, .mimeType = "text/html", .adFrameStatus = .{ .adFrameType = "none", @@ -1073,6 +1101,44 @@ test "cdp.frame: getFrameTree" { } } +test "cdp.frame: child frame metadata" { + var ctx = try testing.context(); + defer ctx.deinit(); + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-FRAME", .url = "cdp/empty_iframe.html", .target_id = "FID-000000000X".* }); + const root = bc.mainFrame() orelse unreachable; + try testing.expectEqual(1, root.child_frames.items.len); + const child = root.child_frames.items[0]; + + const root_id = id.toFrameId(root._frame_id); + const child_id = id.toFrameId(child._frame_id); + const child_loader_id = id.toLoaderId(child._loader_id); + + try ctx.expectSentEvent("Page.frameNavigated", .{ + .frame = .{ + .id = &child_id, + .parentId = &root_id, + .loaderId = &child_loader_id, + .url = "about:blank", + }, + }, .{ .session_id = "SID-X" }); + + try ctx.processMessage(.{ .id = 13, .method = "Page.getFrameTree" }); + try ctx.expectSentResult(.{ + .frameTree = .{ + .frame = .{ .id = &root_id }, + .childFrames = &.{.{ + .frame = .{ + .id = &child_id, + .parentId = &root_id, + .loaderId = &child_loader_id, + .url = "about:blank", + }, + }}, + }, + }, .{ .id = 13 }); +} + test "cdp.frame: frameAttached" { var ctx = try testing.context(); defer ctx.deinit();