mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 17:46:32 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -569,30 +569,29 @@ fn processRootQueuedNavigation(self: *Session) !void {
|
||||
// immediate-swap path for them.
|
||||
const is_synthetic = qn.is_about_blank or std.mem.startsWith(u8, qn.url, "blob:");
|
||||
|
||||
if (is_synthetic) {
|
||||
return self.replaceRootImmediate(current_frame._frame_id, qn);
|
||||
}
|
||||
|
||||
// The qn arena is consumed here regardless of success — frame.navigate
|
||||
// dupes the URL into the page's own arena, so we can release the qn
|
||||
// arena as soon as navigate returns.
|
||||
defer self.arena_pool.release(qn.arena);
|
||||
|
||||
if (is_synthetic) {
|
||||
return self.replaceRootImmediate(current_frame._frame_id, qn.url, qn.opts);
|
||||
}
|
||||
return self.initiateRootNavigation(current_frame._frame_id, qn.url, qn.opts);
|
||||
}
|
||||
|
||||
// Legacy immediate-swap path: tear down the active page and create a new one
|
||||
// in its place before issuing the navigation. Used for synthetic navigations
|
||||
// (about:blank, blob:) where there is no in-flight HTTP and therefore no
|
||||
// "pending" window to span.
|
||||
fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !void {
|
||||
defer self.arena_pool.release(qn.arena);
|
||||
|
||||
// Immediate-swap path for synthetic navigations (about:blank, blob:): there is
|
||||
// no in-flight HTTP and therefore no "pending" window to span — and no
|
||||
// frameHeaderDoneCallback to commit a pending Page. Tear down the active page
|
||||
// and create a new one in its place, then navigate it. Reached from both the
|
||||
// queued-navigation path (processRootQueuedNavigation) and the CDP entry point
|
||||
// (initiateRootNavigation); each caller owns any arena tied to `url`/`opts`.
|
||||
fn replaceRootImmediate(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void {
|
||||
self.tearDownActivePage();
|
||||
const new_frame = try self.installNewActivePage(frame_id);
|
||||
|
||||
new_frame.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err });
|
||||
new_frame.navigate(url, opts) catch |err| {
|
||||
log.err(.browser, "synthetic navigation error", .{ .err = err, .url = url });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
@@ -605,6 +604,14 @@ fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !v
|
||||
pub fn initiateRootNavigation(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void {
|
||||
self.discardPendingPage();
|
||||
|
||||
// Synthetic navigations (about:blank, blob:) have no HTTP round-trip and
|
||||
// therefore no frameHeaderDoneCallback to commit a pending Page. Swap the
|
||||
// active Page immediately instead of allocating a pending one that would
|
||||
// never be promoted, leaving the previous document in place (issue #2363).
|
||||
if (std.mem.eql(u8, "about:blank", url) or std.mem.startsWith(u8, url, "blob:")) {
|
||||
return self.replaceRootImmediate(frame_id, url, opts);
|
||||
}
|
||||
|
||||
const page = try self.allocatePage(frame_id);
|
||||
errdefer self.destroyPage(page);
|
||||
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<head id="the_head">
|
||||
<title>Test Document Title</title>
|
||||
<title>Test File Web API</title>
|
||||
<script src="./testing.js"></script>
|
||||
</head>
|
||||
|
||||
<script id=file>
|
||||
const file = new File();
|
||||
<script id=file_basic>
|
||||
// File requires parts and name.
|
||||
// Verify basic construction.
|
||||
const f = new File(["test data"], "test.txt");
|
||||
|
||||
testing.expectEqual(true, file instanceof File);
|
||||
testing.expectEqual(true, file instanceof Blob);
|
||||
testing.expectEqual(true, f instanceof File);
|
||||
testing.expectEqual(true, f instanceof Blob);
|
||||
testing.expectEqual("test.txt", f.name);
|
||||
testing.expectEqual(9, f.size);
|
||||
testing.expectEqual("", f.type);
|
||||
testing.expectTrue(typeof f.lastModified === 'number');
|
||||
// lastModified should be close to now (within 5 seconds)
|
||||
const now = Date.now();
|
||||
testing.expectTrue(Math.abs(now - f.lastModified) < 5000);
|
||||
</script>
|
||||
|
||||
<script id=file_options type=module>
|
||||
const state = await testing.async();
|
||||
|
||||
// Constructor with full option properties
|
||||
const customTime = 1234567890;
|
||||
const f2 = new File(["foo", "bar"], "data.csv", {
|
||||
type: "text/csv",
|
||||
lastModified: customTime
|
||||
});
|
||||
|
||||
testing.expectEqual("data.csv", f2.name);
|
||||
testing.expectEqual(6, f2.size);
|
||||
testing.expectEqual("text/csv", f2.type);
|
||||
testing.expectEqual(customTime, f2.lastModified);
|
||||
|
||||
// Verify async reader methods (inherited from Blob)
|
||||
const text = await f2.text();
|
||||
state.resolve();
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual("foobar", text);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=file_empty>
|
||||
const fEmpty = new File([], "");
|
||||
testing.expectEqual("", fEmpty.name);
|
||||
testing.expectEqual(0, fEmpty.size);
|
||||
testing.expectEqual("", fEmpty.type);
|
||||
</script>
|
||||
|
||||
@@ -171,7 +171,7 @@ const vector_sizes = blk: {
|
||||
};
|
||||
|
||||
/// Writes a single part with optional line ending normalization.
|
||||
fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
|
||||
pub fn writePartWithEndings(part: []const u8, use_native_endings: bool, writer: *Writer) !void {
|
||||
// Transparent - no conversion needed.
|
||||
if (!use_native_endings) {
|
||||
try writer.writeAll(part);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -27,13 +27,46 @@ const Blob = @import("Blob.zig");
|
||||
const File = @This();
|
||||
|
||||
_proto: *Blob,
|
||||
_name: []const u8,
|
||||
_last_modified: i64,
|
||||
|
||||
// TODO: Implement File API.
|
||||
pub fn init(page: *Page) !*File {
|
||||
const session = page.session;
|
||||
const arena = try session.getArena(.tiny, "File");
|
||||
errdefer session.releaseArena(arena);
|
||||
return page.factory.blob(arena, File{ ._proto = undefined });
|
||||
pub const InitOptions = struct {
|
||||
type: []const u8 = "",
|
||||
endings: []const u8 = "transparent",
|
||||
lastModified: ?i64 = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
parts_: ?[]const js.Value,
|
||||
name: []const u8,
|
||||
opts_: ?InitOptions,
|
||||
page: *Page,
|
||||
) !*File {
|
||||
const opts = opts_ orelse InitOptions{};
|
||||
const blob = try Blob.init(parts_, .{
|
||||
.type = opts.type,
|
||||
.endings = opts.endings,
|
||||
}, page);
|
||||
|
||||
errdefer blob.deinit(page);
|
||||
|
||||
const file = try blob._arena.create(File);
|
||||
file.* = .{
|
||||
._proto = blob,
|
||||
._name = try blob._arena.dupe(u8, name),
|
||||
._last_modified = opts.lastModified orelse std.time.milliTimestamp(),
|
||||
};
|
||||
blob._type = .{ .file = file };
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
pub fn getName(self: *const File) []const u8 {
|
||||
return self._name;
|
||||
}
|
||||
|
||||
pub fn getLastModified(self: *const File) f64 {
|
||||
return @floatFromInt(self._last_modified);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
@@ -46,6 +79,8 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(File.init, .{});
|
||||
pub const name = bridge.accessor(File.getName, null, .{});
|
||||
pub const lastModified = bridge.accessor(File.getLastModified, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -20,6 +20,9 @@ const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Element = @import("Element.zig");
|
||||
|
||||
// This type is only included when the binary is built with the -Dwpt_extensions flag
|
||||
const WebDriver = @This();
|
||||
@@ -30,6 +33,12 @@ pub fn deleteAllCookies(_: *const WebDriver, page: *Page) void {
|
||||
page.session.cookie_jar.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn getComputedLabel(_: *const WebDriver, element: *Element, frame: *Frame) ![]const u8 {
|
||||
const AXNode = @import("../../cdp/AXNode.zig");
|
||||
const axnode = AXNode.fromNode(element.asNode());
|
||||
return (try axnode.getName(frame, frame.call_arena)) orelse "";
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(WebDriver);
|
||||
|
||||
@@ -40,4 +49,5 @@ pub const JsApi = struct {
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
pub const deleteAllCookies = bridge.function(WebDriver.deleteAllCookies, .{});
|
||||
pub const getComputedLabel = bridge.function(WebDriver.getComputedLabel, .{});
|
||||
};
|
||||
|
||||
@@ -1309,6 +1309,42 @@ test "cdp.frame: navigate inherits original fragment across redirect" {
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.frame: navigate to about:blank replaces a non-blank document" {
|
||||
// Regression test for #2363. Page.navigate("about:blank") issued against a
|
||||
// tab that already holds a real document must replace the active document
|
||||
// with a fresh about:blank page — not leave the previous page in place.
|
||||
// A synthetic (no-HTTP) navigation has no response-headers callback to
|
||||
// commit a pending Page, so it must swap the active Page immediately.
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
var bc = try ctx.loadBrowserContext(.{ .id = "BID-AB", .url = "hi.html", .target_id = "TID-AB-0000000".* });
|
||||
|
||||
// Precondition: the tab is on a non-blank document.
|
||||
{
|
||||
const frame = bc.session.currentFrame() orelse unreachable;
|
||||
try testing.expect(std.mem.endsWith(u8, frame.url, "/hi.html"));
|
||||
}
|
||||
|
||||
try ctx.processMessage(.{ .id = 70, .method = "Page.navigate", .params = .{ .url = "about:blank" } });
|
||||
{
|
||||
var runner = try bc.session.runner(.{});
|
||||
try runner.wait(.{ .ms = 2000 });
|
||||
}
|
||||
|
||||
// The active frame must now point at the replaced about:blank document.
|
||||
const frame = bc.session.currentFrame() orelse unreachable;
|
||||
try testing.expectEqualSlices(u8, "about:blank", frame.url);
|
||||
|
||||
// ...and the active page's JS context must agree — the exact symptom in the
|
||||
// bug report was window.location.href staying on the previous URL.
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
const v = try ls.local.exec("window.location.href === 'about:blank'", null);
|
||||
try testing.expect(v.toBool());
|
||||
}
|
||||
|
||||
test "cdp.frame: anchor click sends Referer matching the originating page" {
|
||||
// HTML Living Standard "navigate" algorithm + Fetch §4.5 "request's referrer":
|
||||
// when a navigation is initiated by a hyperlink click (or form submit, or
|
||||
|
||||
@@ -204,8 +204,9 @@ pub const AuthChallenge = struct {
|
||||
.scheme = null,
|
||||
};
|
||||
|
||||
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, value, std.ascii.whitespace[0..]), 0, " ") orelse value.len;
|
||||
const _scheme = value[0..pos];
|
||||
const challenge_value = std.mem.trim(u8, value, std.ascii.whitespace[0..]);
|
||||
const pos = std.mem.indexOfPos(u8, challenge_value, 0, " ") orelse challenge_value.len;
|
||||
const _scheme = challenge_value[0..pos];
|
||||
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
|
||||
ac.scheme = .basic;
|
||||
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
|
||||
|
||||
Reference in New Issue
Block a user