mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-17 08:27:11 -04:00
cdp: Give isolated worlds a context per frame
Previously, we had a single root-bound context which we'd re-announce for every frame. Now, Page.createIsolatedWorld and addScriptToEvaluateOnNewDocument seed the context per frame(s).
This commit is contained in:
4 files changed
+341
-72
No files matched your search
+12
-10
@@ -335,8 +335,8 @@ fn tearDownPage(self: *Session, page: *Page) void {
|
||||
}
|
||||
|
||||
// Allocate a Page in a free slot, publish it as the active page, and
|
||||
// dispatch `frame_created` so CDP creates fresh isolated-world V8
|
||||
// contexts. Used by createPage and by the synthetic-nav path. Does NOT
|
||||
// dispatch `frame_created` so CDP can bind its page handle to the new
|
||||
// frame. Used by createPage and by the synthetic-nav path. Does NOT
|
||||
// dispatch `frame_navigate` — the caller does that (or doesn't, for a
|
||||
// blank initial page).
|
||||
//
|
||||
@@ -351,8 +351,8 @@ fn installNewActivePage(self: *Session, frame_id: u32) !*Frame {
|
||||
errdefer _ = self.pages.pop();
|
||||
|
||||
const frame = &page.frame;
|
||||
// Inform CDP the main frame has been created such that additional
|
||||
// context for other Worlds can be created as well.
|
||||
// Inform CDP the main frame has been created so it can point its page
|
||||
// handle at the new frame.
|
||||
self.notification.dispatch(.frame_created, frame);
|
||||
return frame;
|
||||
}
|
||||
@@ -890,12 +890,14 @@ pub fn initiateRootNavigation(self: *Session, frame_id: u32, url: [:0]const u8,
|
||||
// isolated world contexts plus the node_registry. OLD is still the live
|
||||
// page and its memory is alive (intentional: CDP teardown can walk
|
||||
// old-page state without UAF).
|
||||
// 2. frame_created dispatch — CDP creates fresh isolated world contexts
|
||||
// against the new frame. `replacement.replaces` is still set, so the
|
||||
// session still reports an in-flight nav and CDP's frameCreated skips
|
||||
// its frame_arena reset and captured_responses zeroing (the captured
|
||||
// response for the request we are committing was just inserted by
|
||||
// onHttpResponseHeadersDone moments earlier and must survive).
|
||||
// 2. frame_created dispatch — CDP rebinds its page handle to the new
|
||||
// frame. `replacement.replaces` is still set, so the session still
|
||||
// reports an in-flight nav and CDP's frameCreated skips its frame_arena
|
||||
// reset and captured_responses zeroing (the captured response for the
|
||||
// request we are committing was just inserted by
|
||||
// onHttpResponseHeadersDone moments earlier and must survive). The
|
||||
// isolated worlds emptied in step 1 are NOT refilled here — CDP rebuilds
|
||||
// their contexts on the frame_navigate the caller dispatches afterwards.
|
||||
// 3. Promote: clear `replaces` and unlink OLD from `pages`, so
|
||||
// `currentFrame()` / `livePage()` now resolve to `replacement`. Done AFTER
|
||||
// step 2 so the in-commit signal (replaces != null) survives the dispatch
|
||||
|
||||
+50
-11
@@ -640,18 +640,27 @@ pub const BrowserContext = struct {
|
||||
self.set_child_nodes_sent.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn createIsolatedWorld(self: *BrowserContext, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
|
||||
// The name is the world's identity (matching Chrome). Clients re-issue
|
||||
// this call after every navigation; appending a duplicate each time
|
||||
// would grow the per-page context count without bound.
|
||||
pub const GetOrPutIsolatedWorld = struct {
|
||||
world: *IsolatedWorld,
|
||||
found_existing: bool,
|
||||
};
|
||||
|
||||
pub fn findIsolatedWorld(self: *const BrowserContext, world_name: []const u8) ?*IsolatedWorld {
|
||||
for (self.isolated_worlds.items) |world| {
|
||||
if (std.mem.eql(u8, world.name, world_name)) {
|
||||
if (world.grant_universal_access != grant_universal_access) {
|
||||
log.warn(.cdp, "isolated world mismatch", .{ .name = world_name, .gua = grant_universal_access });
|
||||
}
|
||||
return world;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn createIsolatedWorld(self: *BrowserContext, world_name: []const u8, grant_universal_access: bool) !GetOrPutIsolatedWorld {
|
||||
if (self.findIsolatedWorld(world_name)) |world| {
|
||||
if (world.grant_universal_access != grant_universal_access) {
|
||||
log.warn(.cdp, "isolated world mismatch", .{ .name = world_name, .gua = grant_universal_access });
|
||||
}
|
||||
return .{ .world = world, .found_existing = true };
|
||||
}
|
||||
|
||||
const browser = &self.cdp.browser;
|
||||
const arena = try browser.arena_pool.acquire(.small, "IsolatedWorld");
|
||||
@@ -667,7 +676,19 @@ pub const BrowserContext = struct {
|
||||
|
||||
try self.isolated_worlds.append(self.arena, world);
|
||||
|
||||
return world;
|
||||
return .{ .world = world, .found_existing = false };
|
||||
}
|
||||
|
||||
// only called when we fail to fully create a world (e.g. errdefer in
|
||||
// Page.createIsolatedWorld).
|
||||
pub fn removeIsolatedWorld(self: *BrowserContext, world: *IsolatedWorld) void {
|
||||
for (self.isolated_worlds.items, 0..) |w, i| {
|
||||
if (w == world) {
|
||||
_ = self.isolated_worlds.swapRemove(i);
|
||||
world.deinit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nodeWriter(self: *BrowserContext, root: *const NodeRegistry.Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
@@ -1165,12 +1186,16 @@ pub const BrowserContext = struct {
|
||||
const ScriptOnNewDocument = struct {
|
||||
identifier: u32,
|
||||
source: []const u8,
|
||||
// Page.addScriptToEvaluateOnNewDocument's worldName. null means the main
|
||||
// world. A named world is seeded into every frame (see IsolatedWorld).
|
||||
world_name: ?[]const u8,
|
||||
};
|
||||
|
||||
/// An isolated world is identified by its name and has one V8::Context per
|
||||
// it was requested it. Generally the client needs to resolve a node into the
|
||||
// isolated world to be able to work with it.
|
||||
///
|
||||
/// frame it has been seeded into. A world enters a frame on an explicit
|
||||
/// trigger: Page.createIsolatedWorld or, or a preload script which is seeded
|
||||
/// into every frame. Once seeded, the frame's context is rebuilt on every
|
||||
/// navigation with no further client involvement.
|
||||
/// Frame ids are stable across a child frame's re-navigation (the Frame is
|
||||
/// torn down and re-initialized in place), so a per-frame context is removed
|
||||
/// on frame_destroyed and created again on the frame's next frame_navigated.
|
||||
@@ -1181,6 +1206,9 @@ pub const IsolatedWorld = struct {
|
||||
grant_universal_access: bool,
|
||||
contexts: std.ArrayList(FrameContext) = .empty,
|
||||
|
||||
// Frames this world has been seeded into, by frame id.
|
||||
seeded_frames: std.ArrayList(u32) = .empty,
|
||||
|
||||
// Identity tracking for this isolated world (separate from main world).
|
||||
// Shared by all of the world's frame contexts, like the main world shares
|
||||
// Page.identity across frames, and reset with them on root teardown.
|
||||
@@ -1201,6 +1229,17 @@ pub const IsolatedWorld = struct {
|
||||
self.arena.release();
|
||||
}
|
||||
|
||||
pub fn seed(self: *IsolatedWorld, frame_id: u32) !void {
|
||||
if (self.isSeeded(frame_id)) {
|
||||
return;
|
||||
}
|
||||
return self.seeded_frames.append(self.arena.allocator(), frame_id);
|
||||
}
|
||||
|
||||
pub fn isSeeded(self: *const IsolatedWorld, frame_id: u32) bool {
|
||||
return std.mem.indexOfScalar(u32, self.seeded_frames.items, frame_id) != null;
|
||||
}
|
||||
|
||||
// Keyed by Frame, not frame id: a retired root Page keeps its frame id
|
||||
// while its deferred teardown is pending, and that teardown must not
|
||||
// touch the live page's context.
|
||||
|
||||
+144
-36
@@ -25,14 +25,15 @@ const NodeRegistry = @import("../../../NodeRegistry.zig");
|
||||
|
||||
const dump = @import("../../../browser/dump.zig");
|
||||
const js = @import("../../../browser/js/js.zig");
|
||||
const DOMNode = @import("../../../browser/webapi/Node.zig");
|
||||
const Selector = @import("../../../browser/webapi/selector/Selector.zig");
|
||||
const xpath = @import("../../../browser/xpath/Evaluator.zig");
|
||||
const Input = @import("../../../browser/webapi/element/html/Input.zig");
|
||||
const Page = @import("../../../browser/Page.zig");
|
||||
const Frame = @import("../../../browser/Frame.zig");
|
||||
const File = @import("../../../browser/webapi/File.zig");
|
||||
const Blob = @import("../../../browser/webapi/Blob.zig");
|
||||
const Factory = @import("../../../browser/Factory.zig");
|
||||
const Page = @import("../../../browser/Page.zig");
|
||||
const xpath = @import("../../../browser/xpath/Evaluator.zig");
|
||||
const DOMNode = @import("../../../browser/webapi/Node.zig");
|
||||
const Input = @import("../../../browser/webapi/element/html/Input.zig");
|
||||
const Selector = @import("../../../browser/webapi/selector/Selector.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -343,41 +344,24 @@ fn resolveNode(cmd: *CDP.Command) !void {
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
var ls_open = false;
|
||||
defer if (ls_open) {
|
||||
ls.deinit();
|
||||
};
|
||||
|
||||
if (params.executionContextId) |context_id| blk: {
|
||||
frame.js.localScope(&ls);
|
||||
ls_open = true;
|
||||
if (ls.local.debugContextId() == context_id) {
|
||||
break :blk;
|
||||
}
|
||||
// not the default scope, check the other ones
|
||||
for (bc.isolated_worlds.items) |isolated_world| {
|
||||
for (isolated_world.contexts.items) |fc| {
|
||||
ls.deinit();
|
||||
ls_open = false;
|
||||
|
||||
fc.context.localScope(&ls);
|
||||
ls_open = true;
|
||||
if (ls.local.debugContextId() == context_id) {
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
} else return error.ContextNotFound;
|
||||
} else {
|
||||
frame.js.localScope(&ls);
|
||||
ls_open = true;
|
||||
}
|
||||
const root = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
|
||||
|
||||
// Chrome resolves into the named context, else into the main world of the
|
||||
// node's own document's frame. Drivers adopt a handle found in a child's
|
||||
// utility world into that child's main world this way, so the root's
|
||||
// contexts are not enough.
|
||||
const js_context = if (params.executionContextId) |context_id|
|
||||
findContext(bc, root, context_id) orelse return error.ContextNotFound
|
||||
else
|
||||
nodeFrame(node.dom, root).js;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||
// So we use the Node.Union when retrieve the value from the environment
|
||||
const remote_object = try bc.inspector_session.getRemoteObject(
|
||||
@@ -397,6 +381,51 @@ fn resolveNode(cmd: *CDP.Command) !void {
|
||||
} }, .{});
|
||||
}
|
||||
|
||||
// The frame owning the node's document. Synthetic documents (DOMParser,
|
||||
// DOMImplementation) have no frame and fall back to the root.
|
||||
fn nodeFrame(dom_node: *DOMNode, root: *Frame) *Frame {
|
||||
const document = if (dom_node._type == .document)
|
||||
dom_node.subtype(DOMNode.Document)
|
||||
else
|
||||
dom_node.ownerDocument(root) orelse return root;
|
||||
return document._frame orelse root;
|
||||
}
|
||||
|
||||
// The context the inspector announced under `context_id`: any frame's main
|
||||
// world, then any isolated world's per-frame contexts.
|
||||
fn findContext(bc: *CDP.BrowserContext, root: *Frame, context_id: u32) ?*js.Context {
|
||||
if (findMainWorldContext(root, context_id)) |js_context| {
|
||||
return js_context;
|
||||
}
|
||||
for (bc.isolated_worlds.items) |isolated_world| {
|
||||
for (isolated_world.contexts.items) |fc| {
|
||||
if (contextIdOf(fc.context) == context_id) {
|
||||
return fc.context;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findMainWorldContext(frame: *Frame, context_id: u32) ?*js.Context {
|
||||
if (contextIdOf(frame.js) == context_id) {
|
||||
return frame.js;
|
||||
}
|
||||
for (frame.child_frames.items) |child| {
|
||||
if (findMainWorldContext(child, context_id)) |js_context| {
|
||||
return js_context;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn contextIdOf(js_context: *js.Context) i32 {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
return ls.local.debugContextId();
|
||||
}
|
||||
|
||||
fn describeNode(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
nodeId: ?NodeRegistry.Id = null,
|
||||
@@ -1130,6 +1159,85 @@ test "cdp.dom: querySelector Nodes found" {
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{7} }, .{ .id = 5 });
|
||||
}
|
||||
|
||||
// Drivers find an element in a child frame's utility world, then adopt the
|
||||
// handle into that child's main world with DOM.resolveNode. Both the named
|
||||
// context and the default (the node's own frame) must be the child's, not the
|
||||
// root's.
|
||||
test "cdp.dom: resolveNode into a child frame's context" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-RN", .url = "cdp/isolated_world.html", .target_id = "FID-000000000X".* });
|
||||
const root = bc.mainFrame() orelse unreachable;
|
||||
const child = root.child_frames.items[0];
|
||||
const child_main = try mainWorldContextId(bc, child);
|
||||
try testing.expect(child_main != try mainWorldContextId(bc, root));
|
||||
|
||||
// Register the child's <html> the way DOM.describeNode(objectId) would.
|
||||
const html = child.document.getDocumentElement() orelse unreachable;
|
||||
const node = try bc.node_registry.register(html.asNode());
|
||||
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Runtime.enable", .sessionId = "SID-X" });
|
||||
|
||||
// Into the context the client names.
|
||||
try ctx.processMessage(.{ .id = 11, .method = "DOM.resolveNode", .sessionId = "SID-X", .params = .{
|
||||
.backendNodeId = node.id,
|
||||
.executionContextId = child_main,
|
||||
} });
|
||||
const named = try sentObjectId(&ctx, 11);
|
||||
try ctx.processMessage(.{ .id = 12, .method = "Runtime.callFunctionOn", .sessionId = "SID-X", .params = .{
|
||||
.objectId = named,
|
||||
.functionDeclaration = "function() { return globalThis.document.title + '|' + (this.ownerDocument === globalThis.document); }",
|
||||
.returnByValue = true,
|
||||
} });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "Jobs page one|true" } }, .{ .id = 12 });
|
||||
|
||||
// Into the node's own frame when no context is named.
|
||||
try ctx.processMessage(.{ .id = 13, .method = "DOM.resolveNode", .sessionId = "SID-X", .params = .{
|
||||
.backendNodeId = node.id,
|
||||
} });
|
||||
const default = try sentObjectId(&ctx, 13);
|
||||
try ctx.processMessage(.{ .id = 14, .method = "Runtime.callFunctionOn", .sessionId = "SID-X", .params = .{
|
||||
.objectId = default,
|
||||
.functionDeclaration = "function() { return globalThis.document.title; }",
|
||||
.returnByValue = true,
|
||||
} });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "Jobs page one" } }, .{ .id = 14 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 15, .method = "DOM.resolveNode", .sessionId = "SID-X", .params = .{
|
||||
.backendNodeId = node.id,
|
||||
.executionContextId = 9999,
|
||||
} });
|
||||
try ctx.expectSentError(-31998, "ContextNotFound", .{ .id = 15 });
|
||||
}
|
||||
|
||||
fn mainWorldContextId(bc: *CDP.BrowserContext, frame: *const Frame) !i32 {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
return bc.inspector_session.inspector.getContextId(&ls.local);
|
||||
}
|
||||
|
||||
// The result.object.objectId of the response to command `msg_id`.
|
||||
fn sentObjectId(ctx: *testing.TestContext, msg_id: i64) ![]const u8 {
|
||||
var i: usize = 0;
|
||||
while (try ctx.getSentMessage(i)) |msg| : (i += 1) {
|
||||
const obj = switch (msg) {
|
||||
.object => |o| o,
|
||||
else => continue,
|
||||
};
|
||||
const id_value = obj.get("id") orelse continue;
|
||||
if (id_value != .integer or id_value.integer != msg_id) {
|
||||
continue;
|
||||
}
|
||||
const result = obj.get("result") orelse return error.NoResult;
|
||||
const object = result.object.get("object") orelse return error.NoObject;
|
||||
const object_id = object.object.get("objectId") orelse return error.NoObjectId;
|
||||
return object_id.string;
|
||||
}
|
||||
return error.MessageNotFound;
|
||||
}
|
||||
|
||||
test "cdp.dom: getBoxModel" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
+135
-15
@@ -158,6 +158,18 @@ fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void {
|
||||
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
|
||||
}
|
||||
|
||||
// A worldName registers the world itself:
|
||||
var world_name: ?[]const u8 = null;
|
||||
if (params.worldName) |name| {
|
||||
if (name.len > 0) {
|
||||
const gop = try bc.createIsolatedWorld(name, true);
|
||||
// Borrowed from the world's arena, which outlives the script: a
|
||||
// world is only ever removed on the createIsolatedWorld command's
|
||||
// own error path, before any script can name it.
|
||||
world_name = gop.world.name;
|
||||
}
|
||||
}
|
||||
|
||||
const script_id = bc.next_script_id;
|
||||
bc.next_script_id += 1;
|
||||
|
||||
@@ -165,6 +177,7 @@ fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void {
|
||||
try bc.scripts_on_new_document.append(bc.arena, .{
|
||||
.identifier = script_id,
|
||||
.source = source_dupe,
|
||||
.world_name = world_name,
|
||||
});
|
||||
|
||||
var id_buf: [16]u8 = undefined;
|
||||
@@ -251,7 +264,16 @@ fn createIsolatedWorld(cmd: *CDP.Command) !void {
|
||||
return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{});
|
||||
};
|
||||
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const gop = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const world = gop.world;
|
||||
|
||||
errdefer if (gop.found_existing == false) {
|
||||
bc.removeIsolatedWorld(world);
|
||||
};
|
||||
|
||||
// Seed before creating: frameNavigated only rebuilds contexts for frames
|
||||
// the world was seeded into.
|
||||
try world.seed(frame._frame_id);
|
||||
|
||||
// use the existing world context for a frame if we have it, else create one
|
||||
const js_context = world.contextFor(frame) orelse try createIsolatedWorldContext(cmd.arena, bc, world, frame, null);
|
||||
@@ -272,8 +294,13 @@ fn createIsolatedWorldContext(arena: Allocator, bc: *CDP.BrowserContext, world:
|
||||
}
|
||||
|
||||
// Registers a world context with the inspector, which assigns the id clients
|
||||
// use and sends Runtime.executionContextCreated. Registering a context again
|
||||
// assigns it a new id, so this only happens when the previous id is gone.
|
||||
// use and sends Runtime.executionContextCreated. We may re-register a living
|
||||
// context, the client will get a new id, but both ids are still valid. This
|
||||
// happens when we fast-path via `canNavigateInPlace`, `frameNavigated` still
|
||||
// fires so registerIsolatedWorldContext gets re-called for the same context.
|
||||
// Not the end of the world since it's bound to a single about:blank -> navigate
|
||||
// and necessary since we call executionContextsCleared which tells the client
|
||||
// the old id is invalid
|
||||
fn registerIsolatedWorldContext(arena: Allocator, bc: *CDP.BrowserContext, world: *CDP.IsolatedWorld, js_context: *js.Context, frame: *const Frame, loader_id: ?[]const u8) !void {
|
||||
const frame_id = &id.toFrameId(frame._frame_id);
|
||||
const aux_data = if (loader_id) |lid|
|
||||
@@ -721,7 +748,17 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N
|
||||
is_root_frame,
|
||||
);
|
||||
}
|
||||
// Each known world must get a context per frame
|
||||
// A worldName preload script seeds its world into every frame. This is the
|
||||
// only way for a world to reach a frame besides the explicit Page.createIsolatedWorld.
|
||||
for (bc.scripts_on_new_document.items) |script| {
|
||||
const world = bc.findIsolatedWorld(script.world_name orelse continue) orelse continue;
|
||||
world.seed(frame._frame_id) catch |err| {
|
||||
log.warn(.cdp, "isolated world seed", .{ .err = err, .world = world.name, .frame_id = frame._frame_id });
|
||||
};
|
||||
}
|
||||
|
||||
// Every world seeded into this frame gets a context, rebuilt on each
|
||||
// navigation as blink rebuilds a detached isolated-world window proxy.
|
||||
for (bc.isolated_worlds.items) |isolated_world| {
|
||||
if (isolated_world.contextFor(frame)) |js_context| {
|
||||
// The context was already created ahead of time (createIsolatedWorld).
|
||||
@@ -737,6 +774,10 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isolated_world.isSeeded(frame._frame_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = createIsolatedWorldContext(arena, bc, isolated_world, frame, loader_id) catch |err| {
|
||||
log.warn(.cdp, "isolated world context", .{ .err = err, .world = isolated_world.name, .frame_id = frame._frame_id });
|
||||
};
|
||||
@@ -746,21 +787,24 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N
|
||||
// Must run after the execution context is created but before the client
|
||||
// receives frameNavigated/loadEventFired so polyfills are available for
|
||||
// subsequent CDP commands.
|
||||
if (bc.scripts_on_new_document.items.len > 0) {
|
||||
for (bc.scripts_on_new_document.items) |script| {
|
||||
const js_context = if (script.world_name) |name| blk: {
|
||||
const world = bc.findIsolatedWorld(name) orelse continue;
|
||||
break :blk world.contextFor(frame) orelse continue;
|
||||
} else frame.js;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
js_context.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
for (bc.scripts_on_new_document.items) |script| {
|
||||
var try_catch: lp.js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
var try_catch: lp.js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
ls.local.eval(script.source, null) catch |err| {
|
||||
const caught = try_catch.caughtOrError(arena, err);
|
||||
log.warn(.cdp, "script on new doc", .{ .caught = caught });
|
||||
};
|
||||
}
|
||||
ls.local.eval(script.source, null) catch |err| {
|
||||
const caught = try_catch.caughtOrError(arena, err);
|
||||
log.warn(.cdp, "script on new doc", .{ .caught = caught });
|
||||
};
|
||||
}
|
||||
|
||||
// The DOM.documentUpdated event must be send after the frameNavigated one.
|
||||
@@ -1363,6 +1407,82 @@ test "cdp.frame: createIsolatedWorld targets the requested frame" {
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "Parent jobs" } }, .{ .id = 37 });
|
||||
}
|
||||
|
||||
// Chrome only puts a world in a frame on an explicit trigger. A world the
|
||||
// client asked for on the root must not appear in a sibling frame just
|
||||
// because that frame navigated afterwards.
|
||||
test "cdp.frame: an unseeded frame gets no isolated world context" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-IWS", .url = "cdp/isolated_world.html", .target_id = "FID-000000000X".* });
|
||||
const root = bc.mainFrame() orelse unreachable;
|
||||
const child = root.child_frames.items[0];
|
||||
const root_id = id.toFrameId(root._frame_id);
|
||||
|
||||
try ctx.processMessage(.{ .id = 30, .method = "Runtime.enable", .sessionId = "SID-X" });
|
||||
try ctx.processMessage(.{ .id = 31, .method = "Page.createIsolatedWorld", .params = .{
|
||||
.frameId = &root_id,
|
||||
.worldName = "utility",
|
||||
.grantUniveralAccess = true,
|
||||
} });
|
||||
|
||||
const world = bc.findIsolatedWorld("utility") orelse unreachable;
|
||||
try testing.expect(world.contextFor(root) != null);
|
||||
|
||||
// Navigating the child is what used to create a context for it.
|
||||
try ctx.processMessage(.{ .id = 32, .method = "Runtime.evaluate", .sessionId = "SID-X", .params = .{
|
||||
.expression = "document.querySelector('iframe').src = 'isolated_world_two.html'",
|
||||
} });
|
||||
try testing.waitForPage(bc);
|
||||
|
||||
try testing.expect(world.isSeeded(root._frame_id));
|
||||
try testing.expect(world.isSeeded(child._frame_id) == false);
|
||||
try testing.expect(world.contextFor(child) == null);
|
||||
}
|
||||
|
||||
// A worldName preload script is the one trigger that reaches frames the client
|
||||
// never named, matching blink's InjectScripts. Puppeteer relies on it to give
|
||||
// dynamically-added iframes a utility world.
|
||||
test "cdp.frame: a worldName preload script seeds every frame" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-IWP", .url = "cdp/isolated_world.html", .target_id = "FID-000000000X".* });
|
||||
const root = bc.mainFrame() orelse unreachable;
|
||||
const child = root.child_frames.items[0];
|
||||
|
||||
try ctx.processMessage(.{ .id = 30, .method = "Runtime.enable", .sessionId = "SID-X" });
|
||||
try ctx.processMessage(.{ .id = 31, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{
|
||||
.source = "globalThis.__seeded = 'yes';",
|
||||
.worldName = "utility",
|
||||
} });
|
||||
|
||||
// Registering the script registers the world, but seeds nothing yet.
|
||||
const world = bc.findIsolatedWorld("utility") orelse unreachable;
|
||||
try testing.expect(world.contextFor(child) == null);
|
||||
|
||||
try ctx.processMessage(.{ .id = 32, .method = "Runtime.evaluate", .sessionId = "SID-X", .params = .{
|
||||
.expression = "document.querySelector('iframe').src = 'isolated_world_two.html'",
|
||||
} });
|
||||
try testing.waitForPage(bc);
|
||||
|
||||
// The child was never named in a Page.createIsolatedWorld, but the script
|
||||
// seeded it on navigation.
|
||||
try testing.expect(world.isSeeded(child._frame_id));
|
||||
const child_ctx = try isolatedWorldContextId(bc, child);
|
||||
try ctx.processMessage(.{ .id = 33, .method = "Runtime.evaluate", .sessionId = "SID-X", .params = .{
|
||||
.expression = "__seeded",
|
||||
.contextId = child_ctx,
|
||||
} });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "yes" } }, .{ .id = 33 });
|
||||
|
||||
// ...and ran there, not in the main world.
|
||||
try ctx.processMessage(.{ .id = 34, .method = "Runtime.evaluate", .sessionId = "SID-X", .params = .{
|
||||
.expression = "typeof globalThis.__seeded",
|
||||
} });
|
||||
try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "undefined" } }, .{ .id = 34 });
|
||||
}
|
||||
|
||||
// puppeteer: the utility world is created on the bootstrap about:blank and
|
||||
// must be announced again for the first document, which navigates the
|
||||
// pristine Frame in place (no teardown, no frame_destroyed).
|
||||
|
||||
Reference in new issue
Block a user