diff --git a/build.zig.zon b/build.zig.zon index eb3812a8..b7f9cf3b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,10 +5,10 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz", - .hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz", + .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY", }, - //.v8 = .{ .path = "../zig-v8-fork" }, + // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ // v1.2.0 .url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz", diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 47c63904..9b05c39b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -190,6 +190,8 @@ _queued_navigation: ?*QueuedNavigation = null, // The URL of the current page url: [:0]const u8 = "about:blank", +origin: ?[]const u8 = null, + // The base url specifies the base URL used to resolve the relative urls. // It is set by a tag. // If null the url must be used. @@ -388,10 +390,6 @@ pub fn getTitle(self: *Page) !?[]const u8 { return null; } -pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 { - return try URL.getOrigin(allocator, self.url); -} - // Add comon headers for a request: // * cookies // * referer @@ -449,7 +447,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void { } pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { - const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false; + const current_origin = self.origin orelse return false; return std.mem.startsWith(u8, url, current_origin); } @@ -472,6 +470,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // page and dispatch the events. if (std.mem.eql(u8, "about:blank", request_url)) { self.url = "about:blank"; + + if (self.parent) |parent| { + self.origin = parent.origin; + } else { + self.origin = null; + } + try self.js.setOrigin(self.origin); + // Assume we parsed the document. // It's important to force a reset during the following navigation. self._parse_state = .complete; @@ -518,6 +524,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi var http_client = session.browser.http_client; self.url = try self.arena.dupeZ(u8, request_url); + self.origin = try URL.getOrigin(self.arena, self.url); self._req_id = req_id; self._navigated_options = .{ @@ -825,9 +832,15 @@ fn notifyParentLoadComplete(self: *Page) void { fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool { var self: *Page = @ptrCast(@alignCast(transfer.ctx)); - // would be different than self.url in the case of a redirect const header = &transfer.response_header.?; - self.url = try self.arena.dupeZ(u8, std.mem.span(header.url)); + + const response_url = std.mem.span(header.url); + if (std.mem.eql(u8, response_url, self.url) == false) { + // would be different than self.url in the case of a redirect + self.url = try self.arena.dupeZ(u8, response_url); + self.origin = try URL.getOrigin(self.arena, self.url); + } + try self.js.setOrigin(self.origin); self.window._location = try Location.init(self.url, self); self.document._location = self.window._location; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 9180223c..065b5e28 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -23,6 +23,7 @@ const log = @import("../../log.zig"); const js = @import("js.zig"); const Env = @import("Env.zig"); const bridge = @import("bridge.zig"); +const Origin = @import("Origin.zig"); const Scheduler = @import("Scheduler.zig"); const Page = @import("../Page.zig"); @@ -74,12 +75,7 @@ call_depth: usize = 0, // context.localScope local: ?*const js.Local = null, -// Serves two purposes. Like `global_objects`, this is used to free -// every Global(Object) we've created during the lifetime of the context. -// More importantly, it serves as an identity map - for a given Zig -// instance, we map it to the same Global(Object). -// The key is the @intFromPtr of the Zig value -identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, +origin: *Origin, // Any type that is stored in the identity_map which has a finalizer declared // will have its finalizer stored here. This is only used when shutting down @@ -87,26 +83,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty, finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback), -// Some web APIs have to manage opaque values. Ideally, they use an -// js.Object, but the js.Object has no lifetime guarantee beyond the -// current call. They can call .persist() on their js.Object to get -// a `Global(Object)`. We need to track these to free them. -// This used to be a map and acted like identity_map; the key was -// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without -// a reliable way to know if an object has already been persisted, -// we now simply persist every time persist() is called. -global_values: std.ArrayList(v8.Global) = .empty, -global_objects: std.ArrayList(v8.Global) = .empty, +// Unlike other v8 types, like functions or objects, modules are not shared +// across origins. global_modules: std.ArrayList(v8.Global) = .empty, -global_promises: std.ArrayList(v8.Global) = .empty, -global_functions: std.ArrayList(v8.Global) = .empty, -global_promise_resolvers: std.ArrayList(v8.Global) = .empty, - -// Temp variants stored in HashMaps for O(1) early cleanup. -// Key is global.data_ptr. -global_values_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, -global_promises_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, -global_functions_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, // Our module cache: normalized module specifier => module. module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty, @@ -174,12 +153,6 @@ pub fn deinit(self: *Context) void { // this can release objects self.scheduler.deinit(); - { - var it = self.identity_map.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } { var it = self.finalizer_callbacks.valueIterator(); while (it.next()) |finalizer| { @@ -188,50 +161,11 @@ pub fn deinit(self: *Context) void { self.finalizer_callback_pool.deinit(); } - for (self.global_values.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_objects.items) |*global| { - v8.v8__Global__Reset(global); - } - for (self.global_modules.items) |*global| { v8.v8__Global__Reset(global); } - for (self.global_functions.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_promises.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_promise_resolvers.items) |*global| { - v8.v8__Global__Reset(global); - } - - { - var it = self.global_values_temp.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - - { - var it = self.global_promises_temp.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - - { - var it = self.global_functions_temp.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } + env.releaseOrigin(self.origin); v8.v8__Global__Reset(&self.handle); env.isolate.notifyContextDisposed(); @@ -241,6 +175,38 @@ pub fn deinit(self: *Context) void { v8.v8__MicrotaskQueue__DELETE(self.microtask_queue); } +pub fn setOrigin(self: *Context, key: ?[]const u8) !void { + const env = self.env; + const isolate = env.isolate; + + const origin = try env.getOrCreateOrigin(key); + errdefer env.releaseOrigin(origin); + + try self.origin.transferTo(origin); + self.origin.deinit(env.app); + + self.origin = origin; + + { + var ls: js.Local.Scope = undefined; + self.localScope(&ls); + defer ls.deinit(); + + // Set the V8::Context SecurityToken, which is a big part of what allows + // one context to access another. + const token_local = v8.v8__Global__Get(&origin.security_token, isolate.handle); + v8.v8__Context__SetSecurityToken(ls.local.handle, token_local); + } +} + +pub fn trackGlobal(self: *Context, global: v8.Global) !void { + return self.origin.trackGlobal(global); +} + +pub fn trackTemp(self: *Context, global: v8.Global) !void { + return self.origin.trackTemp(global); +} + pub fn weakRef(self: *Context, obj: anytype) void { const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse { if (comptime IS_DEBUG) { @@ -279,7 +245,7 @@ pub fn release(self: *Context, item: anytype) void { if (@TypeOf(item) == *anyopaque) { // Existing *anyopaque path for identity_map. Called internally from // finalizers - var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse { + var global = self.origin.identity_map.fetchRemove(@intFromPtr(item)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -301,14 +267,14 @@ pub fn release(self: *Context, item: anytype) void { return; } - var map = switch (@TypeOf(item)) { - js.Value.Temp => &self.global_values_temp, - js.Promise.Temp => &self.global_promises_temp, - js.Function.Temp => &self.global_functions_temp, - else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)), - }; + if (comptime IS_DEBUG) { + switch (@TypeOf(item)) { + js.Value.Temp, js.Promise.Temp, js.Function.Temp => {}, + else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)), + } + } - if (map.fetchRemove(item.handle.data_ptr)) |kv| { + if (self.origin.temps.fetchRemove(item.handle.data_ptr)) |kv| { var global = kv.value; v8.v8__Global__Reset(&global); } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 1a86ffd5..9bacc088 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -26,6 +26,7 @@ const App = @import("../../App.zig"); const log = @import("../../log.zig"); const bridge = @import("bridge.zig"); +const Origin = @import("Origin.zig"); const Context = @import("Context.zig"); const Isolate = @import("Isolate.zig"); const Platform = @import("Platform.zig"); @@ -57,6 +58,8 @@ const Env = @This(); app: *App, +allocator: Allocator, + platform: *const Platform, // the global isolate @@ -70,6 +73,9 @@ isolate_params: *v8.CreateParams, context_id: usize, +// Maps origin -> shared Origin contains, for v8 values shared across same-origin Contexts +origins: std.StringHashMapUnmanaged(*Origin) = .empty, + // Global handles that need to be freed on deinit eternal_function_templates: []v8.Eternal, @@ -206,6 +212,7 @@ pub fn init(app: *App, opts: InitOpts) !Env { return .{ .app = app, .context_id = 0, + .allocator = allocator, .contexts = undefined, .context_count = 0, .isolate = isolate, @@ -228,7 +235,17 @@ pub fn deinit(self: *Env) void { ctx.deinit(); } - const allocator = self.app.allocator; + const app = self.app; + const allocator = app.allocator; + + { + var it = self.origins.valueIterator(); + while (it.next()) |value| { + value.*.deinit(app); + } + self.origins.deinit(allocator); + } + if (self.inspector) |i| { i.deinit(allocator); } @@ -272,6 +289,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context { // get the global object for the context, this maps to our Window const global_obj = v8.v8__Context__Global(v8_context).?; + { // Store our TAO inside the internal field of the global object. This // maps the v8::Object -> Zig instance. Almost all objects have this, and @@ -287,6 +305,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context { }; v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); } + // our window wrapped in a v8::Global var global_global: v8.Global = undefined; v8.v8__Global__New(isolate.handle, global_obj, &global_global); @@ -294,10 +313,14 @@ pub fn createContext(self: *Env, page: *Page) !*Context { const context_id = self.context_id; self.context_id = context_id + 1; + const origin = try self.getOrCreateOrigin(null); + errdefer self.releaseOrigin(origin); + const context = try context_arena.create(Context); context.* = .{ .env = self, .page = page, + .origin = origin, .id = context_id, .isolate = isolate, .arena = context_arena, @@ -309,7 +332,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context { .scheduler = .init(context_arena), .finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator), }; - try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); + try context.origin.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); // Store a pointer to our context inside the v8 context so that, given // a v8 context, we can get our context out @@ -350,6 +373,41 @@ pub fn destroyContext(self: *Env, context: *Context) void { context.deinit(); } +pub fn getOrCreateOrigin(self: *Env, key_: ?[]const u8) !*Origin { + const key = key_ orelse { + var opaque_origin: [36]u8 = undefined; + @import("../../id.zig").uuidv4(&opaque_origin); + // Origin.init will dupe opaque_origin. It's fine that this doesn't + // get added to self.origins. In fact, it further isolates it. When the + // context is freed, it'll call env.releaseOrigin which will free it. + return Origin.init(self.app, self.isolate, &opaque_origin); + }; + + const gop = try self.origins.getOrPut(self.allocator, key); + if (gop.found_existing) { + const origin = gop.value_ptr.*; + origin.rc += 1; + return origin; + } + + errdefer _ = self.origins.remove(key); + + const origin = try Origin.init(self.app, self.isolate, key); + gop.key_ptr.* = origin.key; + gop.value_ptr.* = origin; + return origin; +} + +pub fn releaseOrigin(self: *Env, origin: *Origin) void { + const rc = origin.rc; + if (rc == 1) { + _ = self.origins.remove(origin.key); + origin.deinit(self.app); + } else { + origin.rc = rc - 1; + } +} + pub fn runMicrotasks(self: *Env) void { if (self.microtask_queues_are_running == false) { const v8_isolate = self.isolate.handle; diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 01243d35..203fd9ab 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -209,9 +209,9 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { - try ctx.global_functions.append(ctx.arena, global); + try ctx.trackGlobal(global); } else { - try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global); + try ctx.trackTemp(global); } return .{ .handle = global }; } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 6a68b332..305391f5 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -171,7 +171,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, .pointer => |ptr| { const resolved = resolveValue(value); - const gop = try ctx.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); + const gop = try ctx.origin.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); if (gop.found_existing) { // we've seen this instance before, return the same object return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 981f4a2b..fbf036e4 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -97,7 +97,7 @@ pub fn persist(self: Object) !Global { var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_objects.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/js/Origin.zig b/src/browser/js/Origin.zig new file mode 100644 index 00000000..6cb0b356 --- /dev/null +++ b/src/browser/js/Origin.zig @@ -0,0 +1,148 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Origin represents the shared Zig<->JS bridge state for all contexts within +// the same origin. Multiple contexts (frames) from the same origin share a +// single Origin, ensuring that JS objects maintain their identity across frames. + +const std = @import("std"); +const js = @import("js.zig"); + +const App = @import("../../App.zig"); + +const v8 = js.v8; +const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("build").mode == .Debug; + +const Origin = @This(); + +rc: usize = 1, +arena: Allocator, + +// The key, e.g. lightpanda.io:443 +key: []const u8, + +// Security token - all contexts in this realm must use the same v8::Value instance +// as their security token for V8 to allow cross-context access +security_token: v8.Global, + +// Serves two purposes. Like `global_objects`, this is used to free +// every Global(Object) we've created during the lifetime of the realm. +// More importantly, it serves as an identity map - for a given Zig +// instance, we map it to the same Global(Object). +// The key is the @intFromPtr of the Zig value +identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +// Some web APIs have to manage opaque values. Ideally, they use an +// js.Object, but the js.Object has no lifetime guarantee beyond the +// current call. They can call .persist() on their js.Object to get +// a `Global(Object)`. We need to track these to free them. +// This used to be a map and acted like identity_map; the key was +// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without +// a reliable way to know if an object has already been persisted, +// we now simply persist every time persist() is called. +globals: std.ArrayList(v8.Global) = .empty, + +// Temp variants stored in HashMaps for O(1) early cleanup. +// Key is global.data_ptr. +temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { + const arena = try app.arena_pool.acquire(); + errdefer app.arena_pool.release(arena); + + var hs: js.HandleScope = undefined; + hs.init(isolate); + defer hs.deinit(); + + const owned_key = try arena.dupe(u8, key); + const token_local = isolate.initStringHandle(owned_key); + var token_global: v8.Global = undefined; + v8.v8__Global__New(isolate.handle, token_local, &token_global); + + const self = try arena.create(Origin); + self.* = .{ + .rc = 1, + .arena = arena, + .key = owned_key, + .globals = .empty, + .temps = .empty, + .security_token = token_global, + }; + return self; +} + +pub fn deinit(self: *Origin, app: *App) void { + v8.v8__Global__Reset(&self.security_token); + + { + var it = self.identity_map.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } + + for (self.globals.items) |*global| { + v8.v8__Global__Reset(global); + } + + { + var it = self.temps.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } + + app.arena_pool.release(self.arena); +} + +pub fn trackGlobal(self: *Origin, global: v8.Global) !void { + return self.globals.append(self.arena, global); +} + +pub fn trackTemp(self: *Origin, global: v8.Global) !void { + return self.temps.put(self.arena, global.data_ptr, global); +} + +pub fn transferTo(self: *Origin, dest: *Origin) !void { + const arena = dest.arena; + + try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len); + for (self.globals.items) |obj| { + dest.globals.appendAssumeCapacity(obj); + } + self.globals.clearRetainingCapacity(); + + { + try dest.temps.ensureUnusedCapacity(arena, self.temps.count()); + var it = self.temps.iterator(); + while (it.next()) |kv| { + try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*); + } + self.temps.clearRetainingCapacity(); + } + + { + try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count()); + var it = self.identity_map.iterator(); + while (it.next()) |kv| { + try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*); + } + self.identity_map.clearRetainingCapacity(); + } +} diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index afadbe82..98520d4b 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -62,9 +62,9 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { - try ctx.global_promises.append(ctx.arena, global); + try ctx.trackGlobal(global); } else { - try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global); + try ctx.trackTemp(global); } return .{ .handle = global }; } diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index 183effee..f2aac0e0 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -79,7 +79,7 @@ pub fn persist(self: PromiseResolver) !Global { var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_promise_resolvers.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 7963ae7c..fbc961ed 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -259,9 +259,9 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { - try ctx.global_values.append(ctx.arena, global); + try ctx.trackGlobal(global); } else { - try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global); + try ctx.trackTemp(global); } return .{ .handle = global }; } diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 9415b717..22651c39 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -161,7 +161,7 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type { var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_values.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/tests/frames/frames.html b/src/browser/tests/frames/frames.html index 4e614de9..97bed281 100644 --- a/src/browser/tests/frames/frames.html +++ b/src/browser/tests/frames/frames.html @@ -64,11 +64,12 @@ // child frame's top.parent is itself (root has no parent) testing.expectEqual(window, window[0].top.parent); - // Todo: Context security tokens - // testing.expectEqual(true, window.sub1_loaded); - // testing.expectEqual(true, window.sub2_loaded); - // testing.expectEqual(1, window.sub1_count); - // testing.expectEqual(2, window.sub2_count); + // Cross-frame property access + testing.expectEqual(true, window.sub1_loaded); + testing.expectEqual(true, window.sub2_loaded); + testing.expectEqual(1, window.sub1_count); + // depends on how far the initial load got before it was cancelled. + testing.expectEqual(true, window.sub2_count == 1 || window.sub2_count == 2); }); diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index 90434f0f..987ba042 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -118,7 +118,7 @@ BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/', }; - if (!IS_TEST_RUNNER) { + if (window.navigator.userAgent.startsWith("Lightpanda/") == false) { // The page is running in a different browser. Probably a developer making sure // a test is correct. There are a few tweaks we need to do to make this a // seemless, namely around adapting paths/urls. diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 3bc6f586..fda7d2a5 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -243,11 +243,10 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { var uuid_buf: [36]u8 = undefined; @import("../../id.zig").uuidv4(&uuid_buf); - const origin = (try page.getOrigin(page.call_arena)) orelse "null"; const blob_url = try std.fmt.allocPrint( page.arena, "blob:{s}/{s}", - .{ origin, uuid_buf }, + .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); return blob_url; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index a96ac2b6..6e406c05 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -414,7 +414,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P bc.inspector_session.inspector.contextCreated( &ls.local, "", - try page.getOrigin(arena) orelse "", + page.origin orelse "", aux_data, true, ); diff --git a/src/testing.zig b/src/testing.zig index a398f824..774f76e4 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -414,15 +414,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void { try_catch.init(&ls.local); defer try_catch.deinit(); - // by default, on load, testing.js will call testing.assertOk(). This makes our - // tests work well in a browser. But, for our test runner, we disable that - // and call it explicitly. This gives us better error messages. - ls.local.eval("window._lightpanda_skip_auto_assert = true;", "auto_assert") catch |err| { - const caught = try_catch.caughtOrError(arena_allocator, err); - std.debug.print("disable auto assert failure\nError: {f}\n", .{caught}); - return err; - }; - try page.navigate(url, .{}); _ = test_session.wait(2000);