mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 20:54:43 +00:00
Merge pull request #1253 from lightpanda-io/zigdom-navigation
Backport Navigation (and friends)
This commit is contained in:
@@ -119,7 +119,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
|||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
||||||
.xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue => {
|
.xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation => {
|
||||||
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
try self.dispatchAll(list, target, event, &was_handled);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const Mime = @import("Mime.zig");
|
|||||||
const Factory = @import("Factory.zig");
|
const Factory = @import("Factory.zig");
|
||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
const Scheduler = @import("Scheduler.zig");
|
const Scheduler = @import("Scheduler.zig");
|
||||||
const History = @import("webapi/History.zig");
|
|
||||||
const EventManager = @import("EventManager.zig");
|
const EventManager = @import("EventManager.zig");
|
||||||
const ScriptManager = @import("ScriptManager.zig");
|
const ScriptManager = @import("ScriptManager.zig");
|
||||||
|
|
||||||
@@ -58,6 +57,8 @@ const MutationObserver = @import("webapi/MutationObserver.zig");
|
|||||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||||
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
|
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
||||||
|
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||||
|
|
||||||
const timestamp = @import("../datetime.zig").timestamp;
|
const timestamp = @import("../datetime.zig").timestamp;
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||||
@@ -211,7 +212,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
|||||||
self.window = try self._factory.eventTarget(Window{
|
self.window = try self._factory.eventTarget(Window{
|
||||||
._document = self.document,
|
._document = self.document,
|
||||||
._storage_bucket = storage_bucket,
|
._storage_bucket = storage_bucket,
|
||||||
._history = History.init(self),
|
|
||||||
._performance = Performance.init(),
|
._performance = Performance.init(),
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._location = &default_location,
|
._location = &default_location,
|
||||||
@@ -273,7 +273,33 @@ fn registerBackgroundTasks(self: *Page) !void {
|
|||||||
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
|
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts, kind: NavigationKind) !void {
|
||||||
|
const session = self._session;
|
||||||
|
|
||||||
|
const resolved_url = try URL.resolve(
|
||||||
|
session.transfer_arena,
|
||||||
|
self.url,
|
||||||
|
request_url,
|
||||||
|
.{ .always_dupe = true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// setting opts.force = true will force a page load.
|
||||||
|
// otherwise, we will need to ensure this is a true (not document) navigation.
|
||||||
|
if (!opts.force) {
|
||||||
|
// If we are navigating within the same document, just change URL.
|
||||||
|
if (URL.eqlDocument(self.url, resolved_url)) {
|
||||||
|
// update page url
|
||||||
|
self.url = resolved_url;
|
||||||
|
|
||||||
|
// update location
|
||||||
|
self.window._location = try Location.init(self.url, self);
|
||||||
|
self.document._location = self.window._location;
|
||||||
|
|
||||||
|
try session.navigation.updateEntries(resolved_url, kind, self, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (self._parse_state != .pre) {
|
if (self._parse_state != .pre) {
|
||||||
// it's possible for navigate to be called multiple times on the
|
// it's possible for navigate to be called multiple times on the
|
||||||
// same page (via CDP). We want to reset the page between each call.
|
// same page (via CDP). We want to reset the page between each call.
|
||||||
@@ -330,6 +356,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
.timestamp = timestamp(.monotonic),
|
.timestamp = timestamp(.monotonic),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
session.navigation._current_navigation_kind = kind;
|
||||||
|
|
||||||
http_client.request(.{
|
http_client.request(.{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
@@ -414,6 +442,14 @@ fn _documentIsComplete(self: *Page) !void {
|
|||||||
self.window._on_load,
|
self.window._on_load,
|
||||||
.{ .inject_target = false, .context = "page load" },
|
.{ .inject_target = false, .context = "page load" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pageshow_event = try PageTransitionEvent.init("pageshow", .{}, self);
|
||||||
|
try self._event_manager.dispatchWithFunction(
|
||||||
|
self.window.asEventTarget(),
|
||||||
|
pageshow_event.asEvent(),
|
||||||
|
self.window._on_pageshow,
|
||||||
|
.{ .context = "page show" },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
|
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
|
||||||
@@ -495,6 +531,9 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||||
self.clearTransferArena();
|
self.clearTransferArena();
|
||||||
|
|
||||||
|
//We need to handle different navigation types differently.
|
||||||
|
try self._session.navigation.commitNavigation(self);
|
||||||
|
|
||||||
defer if (comptime IS_DEBUG) {
|
defer if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "page.load.complete", .{ .url = self.url });
|
log.debug(.page, "page.load.complete", .{ .url = self.url });
|
||||||
};
|
};
|
||||||
@@ -530,9 +569,6 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
// We need to handle different navigation types differently.
|
|
||||||
// @ZIGDOM
|
|
||||||
// try self._session.navigation.processNavigation(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
@@ -1879,12 +1915,19 @@ const IdleNotification = union(enum) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
||||||
|
const URLRaw = @import("URL.zig");
|
||||||
|
const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false;
|
||||||
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
|
}
|
||||||
|
|
||||||
pub const NavigateReason = enum {
|
pub const NavigateReason = enum {
|
||||||
anchor,
|
anchor,
|
||||||
address_bar,
|
address_bar,
|
||||||
form,
|
form,
|
||||||
script,
|
script,
|
||||||
history,
|
history,
|
||||||
|
navigation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NavigateOpts = struct {
|
pub const NavigateOpts = struct {
|
||||||
@@ -1893,6 +1936,7 @@ pub const NavigateOpts = struct {
|
|||||||
method: Http.Method = .GET,
|
method: Http.Method = .GET,
|
||||||
body: ?[]const u8 = null,
|
body: ?[]const u8 = null,
|
||||||
header: ?[:0]const u8 = null,
|
header: ?[:0]const u8 = null,
|
||||||
|
force: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RequestCookieOpts = struct {
|
const RequestCookieOpts = struct {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const log = @import("../log.zig");
|
|||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
|
const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||||
|
const History = @import("webapi/History.zig");
|
||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
@@ -54,6 +56,9 @@ executor: js.ExecutionWorld,
|
|||||||
cookie_jar: storage.Cookie.Jar,
|
cookie_jar: storage.Cookie.Jar,
|
||||||
storage_shed: storage.Shed,
|
storage_shed: storage.Shed,
|
||||||
|
|
||||||
|
history: History,
|
||||||
|
navigation: Navigation,
|
||||||
|
|
||||||
page: ?*Page = null,
|
page: ?*Page = null,
|
||||||
|
|
||||||
// If the current page want to navigate to a new page
|
// If the current page want to navigate to a new page
|
||||||
@@ -67,13 +72,17 @@ pub fn init(self: *Session, browser: *Browser) !void {
|
|||||||
errdefer executor.deinit();
|
errdefer executor.deinit();
|
||||||
|
|
||||||
const allocator = browser.app.allocator;
|
const allocator = browser.app.allocator;
|
||||||
|
const session_allocator = browser.session_arena.allocator();
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.browser = browser,
|
.browser = browser,
|
||||||
.executor = executor,
|
.executor = executor,
|
||||||
.storage_shed = .{},
|
.storage_shed = .{},
|
||||||
.queued_navigation = null,
|
.queued_navigation = null,
|
||||||
.arena = browser.session_arena.allocator(),
|
.arena = session_allocator,
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
|
.navigation = Navigation.init(session_allocator),
|
||||||
|
.history = .{},
|
||||||
.transfer_arena = browser.transfer_arena.allocator(),
|
.transfer_arena = browser.transfer_arena.allocator(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -98,6 +107,9 @@ pub fn createPage(self: *Session) !*Page {
|
|||||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
||||||
const page = self.page.?;
|
const page = self.page.?;
|
||||||
|
|
||||||
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(page);
|
||||||
|
|
||||||
log.debug(.browser, "create page", .{});
|
log.debug(.browser, "create page", .{});
|
||||||
// start JS env
|
// start JS env
|
||||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
@@ -115,6 +127,8 @@ pub fn removePage(self: *Session) void {
|
|||||||
self.page.?.deinit();
|
self.page.?.deinit();
|
||||||
self.page = null;
|
self.page = null;
|
||||||
|
|
||||||
|
self.navigation.onRemovePage();
|
||||||
|
|
||||||
log.debug(.browser, "remove page", .{});
|
log.debug(.browser, "remove page", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +191,11 @@ fn processQueuedNavigation(self: *Session) !bool {
|
|||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
page.navigate(qn.url, qn.opts) catch |err| {
|
page.navigate(
|
||||||
|
qn.url,
|
||||||
|
qn.opts,
|
||||||
|
self.navigation._current_navigation_kind orelse .{ .push = null },
|
||||||
|
) catch |err| {
|
||||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized_base: []const u8 = base;
|
var normalized_base: []const u8 = base[0..path_start];
|
||||||
if (std.mem.lastIndexOfScalar(u8, normalized_base[authority_start..], '/')) |pos| {
|
if (path_start < base.len) {
|
||||||
normalized_base = normalized_base[0 .. pos + authority_start];
|
if (std.mem.lastIndexOfScalar(u8, base[path_start + 1 ..], '/')) |pos| {
|
||||||
|
normalized_base = base[0 .. path_start + 1 + pos];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// trailing space so that we always have space to append the null terminator
|
// trailing space so that we always have space to append the null terminator
|
||||||
@@ -268,6 +270,14 @@ pub fn getHost(raw: [:0]const u8) []const u8 {
|
|||||||
return authority[0..path_start];
|
return authority[0..path_start];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if these two URLs point to the same document.
|
||||||
|
pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool {
|
||||||
|
// First '#' signifies the start of the fragment.
|
||||||
|
const first_hash_index = std.mem.indexOfScalar(u8, first, '#') orelse first.len;
|
||||||
|
const second_hash_index = std.mem.indexOfScalar(u8, second, '#') orelse second.len;
|
||||||
|
return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]);
|
||||||
|
}
|
||||||
|
|
||||||
const KnownProtocol = enum {
|
const KnownProtocol = enum {
|
||||||
@"http:",
|
@"http:",
|
||||||
@"https:",
|
@"https:",
|
||||||
@@ -286,6 +296,29 @@ test "URL: isCompleteHTTPUrl" {
|
|||||||
try testing.expectEqual(false, isCompleteHTTPUrl("about"));
|
try testing.expectEqual(false, isCompleteHTTPUrl("about"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: resolve regression (#1093)" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
base: [:0]const u8,
|
||||||
|
path: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.base = "https://alas.aws.amazon.com/alas2.html",
|
||||||
|
.path = "../static/bootstrap.min.css",
|
||||||
|
.expected = "https://alas.aws.amazon.com/static/bootstrap.min.css",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "URL: resolve" {
|
test "URL: resolve" {
|
||||||
defer testing.reset();
|
defer testing.reset();
|
||||||
|
|
||||||
@@ -413,3 +446,71 @@ test "URL: resolve" {
|
|||||||
try testing.expectString(case.expected, result);
|
try testing.expectString(case.expected, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: eqlDocument" {
|
||||||
|
defer testing.reset();
|
||||||
|
{
|
||||||
|
const url = "https://lightpanda.io/about";
|
||||||
|
try testing.expectEqual(true, eqlDocument(url, url));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about";
|
||||||
|
const url2 = "http://lightpanda.io/about";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about";
|
||||||
|
const url2 = "https://example.com/about";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io:8080/about";
|
||||||
|
const url2 = "https://lightpanda.io:9090/about";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about";
|
||||||
|
const url2 = "https://lightpanda.io/contact";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about?foo=bar";
|
||||||
|
const url2 = "https://lightpanda.io/about?baz=qux";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about#section1";
|
||||||
|
const url2 = "https://lightpanda.io/about#section2";
|
||||||
|
try testing.expectEqual(true, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about";
|
||||||
|
const url2 = "https://lightpanda.io/about/";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about?foo=bar";
|
||||||
|
const url2 = "https://lightpanda.io/about";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about";
|
||||||
|
const url2 = "https://lightpanda.io/about?foo=bar";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about?foo=bar";
|
||||||
|
const url2 = "https://lightpanda.io/about?foo=bar";
|
||||||
|
try testing.expectEqual(true, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://lightpanda.io/about?";
|
||||||
|
const url2 = "https://lightpanda.io/about";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const url1 = "https://duckduckgo.com/";
|
||||||
|
const url2 = "https://duckduckgo.com/?q=lightpanda";
|
||||||
|
try testing.expectEqual(false, eqlDocument(url1, url2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (T == js.Value) {
|
if (T == js.Value) {
|
||||||
return value.value;
|
return value.js_val;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (T == js.Promise) {
|
if (T == js.Promise) {
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
|||||||
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
|
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
|
||||||
|
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
||||||
|
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||||
|
return Value{ .context = ctx, .js_val = value };
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toObject(self: Value) js.Object {
|
pub fn toObject(self: Value) js.Object {
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.context = self.context,
|
||||||
|
|||||||
@@ -567,6 +567,9 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/ErrorEvent.zig"),
|
@import("../webapi/event/ErrorEvent.zig"),
|
||||||
@import("../webapi/event/MessageEvent.zig"),
|
@import("../webapi/event/MessageEvent.zig"),
|
||||||
@import("../webapi/event/ProgressEvent.zig"),
|
@import("../webapi/event/ProgressEvent.zig"),
|
||||||
|
@import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"),
|
||||||
|
@import("../webapi/event/PageTransitionEvent.zig"),
|
||||||
|
@import("../webapi/event/PopStateEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
@import("../webapi/media/MediaError.zig"),
|
@import("../webapi/media/MediaError.zig"),
|
||||||
@@ -599,4 +602,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/File.zig"),
|
@import("../webapi/File.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.zig"),
|
||||||
@import("../webapi/PerformanceObserver.zig"),
|
@import("../webapi/PerformanceObserver.zig"),
|
||||||
|
@import("../webapi/navigation/Navigation.zig"),
|
||||||
|
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
||||||
|
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
||||||
|
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,3 +5,21 @@
|
|||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', window.location.href);
|
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', window.location.href);
|
||||||
testing.expectEqual(document.location, window.location);
|
testing.expectEqual(document.location, window.location);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=location_hash>
|
||||||
|
location.hash = "";
|
||||||
|
testing.expectEqual("", location.hash);
|
||||||
|
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', location.href);
|
||||||
|
|
||||||
|
location.hash = "#abcdef";
|
||||||
|
testing.expectEqual("#abcdef", location.hash);
|
||||||
|
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html#abcdef', location.href);
|
||||||
|
|
||||||
|
location.hash = "xyzxyz";
|
||||||
|
testing.expectEqual("#xyzxyz", location.hash);
|
||||||
|
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html#xyzxyz', location.href);
|
||||||
|
|
||||||
|
location.hash = "";
|
||||||
|
testing.expectEqual("", location.hash);
|
||||||
|
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', location.href);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ pub const Type = union(enum) {
|
|||||||
message_event: *@import("event/MessageEvent.zig"),
|
message_event: *@import("event/MessageEvent.zig"),
|
||||||
progress_event: *@import("event/ProgressEvent.zig"),
|
progress_event: *@import("event/ProgressEvent.zig"),
|
||||||
composition_event: *@import("event/CompositionEvent.zig"),
|
composition_event: *@import("event/CompositionEvent.zig"),
|
||||||
|
navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"),
|
||||||
|
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
||||||
|
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub const Type = union(enum) {
|
|||||||
media_query_list: *@import("css/MediaQueryList.zig"),
|
media_query_list: *@import("css/MediaQueryList.zig"),
|
||||||
message_port: *@import("MessagePort.zig"),
|
message_port: *@import("MessagePort.zig"),
|
||||||
text_track_cue: *@import("media/TextTrackCue.zig"),
|
text_track_cue: *@import("media/TextTrackCue.zig"),
|
||||||
|
navigation: *@import("navigation/NavigationEventTarget.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
|
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
|
||||||
|
|||||||
@@ -20,67 +20,75 @@ const std = @import("std");
|
|||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const PopStateEvent = @import("event/PopStateEvent.zig");
|
||||||
|
|
||||||
const History = @This();
|
const History = @This();
|
||||||
|
|
||||||
_page: *Page,
|
pub fn getLength(_: *const History, page: *Page) u32 {
|
||||||
_length: u32 = 1,
|
return @intCast(page._session.navigation._entries.items.len);
|
||||||
_state: ?js.Object = null,
|
|
||||||
|
|
||||||
pub fn init(page: *Page) History {
|
|
||||||
return .{
|
|
||||||
._page = page,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *History) void {
|
pub fn getState(_: *const History, page: *Page) !?js.Value {
|
||||||
if (self._state) |state| {
|
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
|
||||||
js.q.JS_FreeValue(self._page.js.ctx, state.value);
|
const value = try js.Value.fromJson(page.js, state);
|
||||||
|
return value;
|
||||||
|
} else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
|
const arena = page._session.arena;
|
||||||
|
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
|
||||||
|
|
||||||
|
const json = state.toJson(arena) catch return error.DateClone;
|
||||||
|
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
|
const arena = page._session.arena;
|
||||||
|
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
|
||||||
|
|
||||||
|
const json = state.toJson(arena) catch return error.DateClone;
|
||||||
|
_ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goInner(delta: i32, page: *Page) !void {
|
||||||
|
// 0 behaves the same as no argument, both reloadig the page.
|
||||||
|
|
||||||
|
const current = page._session.navigation._index;
|
||||||
|
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||||
|
if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const index = @as(usize, @intCast(index_s));
|
||||||
|
const entry = page._session.navigation._entries.items[index];
|
||||||
|
|
||||||
|
if (entry._url) |url| {
|
||||||
|
if (try page.isSameOrigin(url)) {
|
||||||
|
const event = try PopStateEvent.init("popstate", .{ .state = entry._state.value }, page);
|
||||||
|
|
||||||
|
try page._event_manager.dispatchWithFunction(
|
||||||
|
page.window.asEventTarget(),
|
||||||
|
event.asEvent(),
|
||||||
|
page.window._on_popstate,
|
||||||
|
.{ .context = "Pop State" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getLength(self: *const History) u32 {
|
pub fn back(_: *History, page: *Page) !void {
|
||||||
return self._length;
|
try goInner(-1, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getState(self: *const History) ?js.Object {
|
pub fn forward(_: *History, page: *Page) !void {
|
||||||
return self._state;
|
try goInner(1, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
|
pub fn go(_: *History, delta: ?i32, page: *Page) !void {
|
||||||
_ = _title; // title is ignored in modern browsers
|
try goInner(delta orelse 0, page);
|
||||||
_ = url; // For minimal implementation, we don't actually navigate
|
|
||||||
_ = page;
|
|
||||||
|
|
||||||
self._state = try state.persist();
|
|
||||||
self._length += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
|
|
||||||
_ = _title;
|
|
||||||
_ = url;
|
|
||||||
_ = page;
|
|
||||||
self._state = try state.persist();
|
|
||||||
// Note: replaceState doesn't change length
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn back(self: *History, page: *Page) void {
|
|
||||||
_ = self;
|
|
||||||
_ = page;
|
|
||||||
// Minimal implementation: no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn forward(self: *History, page: *Page) void {
|
|
||||||
_ = self;
|
|
||||||
_ = page;
|
|
||||||
// Minimal implementation: no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn go(self: *History, delta: i32, page: *Page) void {
|
|
||||||
_ = self;
|
|
||||||
_ = delta;
|
|
||||||
_ = page;
|
|
||||||
// Minimal implementation: no-op
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
@@ -64,6 +65,25 @@ pub fn getHash(self: *const Location) []const u8 {
|
|||||||
return self._url.getHash();
|
return self._url.getHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {
|
||||||
|
const normalized_hash = blk: {
|
||||||
|
if (hash.len == 0) {
|
||||||
|
const old_url = page.url;
|
||||||
|
|
||||||
|
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
|
||||||
|
old_url[0..index]
|
||||||
|
else
|
||||||
|
old_url;
|
||||||
|
} else if (hash[0] == '#')
|
||||||
|
break :blk hash
|
||||||
|
else
|
||||||
|
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
|
||||||
|
};
|
||||||
|
|
||||||
|
const duped_hash = try page.arena.dupeZ(u8, normalized_hash);
|
||||||
|
return page.navigate(duped_hash, .{ .reason = .script }, .{ .replace = null });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {
|
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {
|
||||||
return self._url.toString(page);
|
return self._url.toString(page);
|
||||||
}
|
}
|
||||||
@@ -80,7 +100,7 @@ pub const JsApi = struct {
|
|||||||
pub const toString = bridge.function(Location.toString, .{});
|
pub const toString = bridge.function(Location.toString, .{});
|
||||||
pub const href = bridge.accessor(Location.toString, null, .{});
|
pub const href = bridge.accessor(Location.toString, null, .{});
|
||||||
pub const search = bridge.accessor(Location.getSearch, null, .{});
|
pub const search = bridge.accessor(Location.getSearch, null, .{});
|
||||||
pub const hash = bridge.accessor(Location.getHash, null, .{});
|
pub const hash = bridge.accessor(Location.getHash, Location.setHash, .{});
|
||||||
pub const pathname = bridge.accessor(Location.getPathname, null, .{});
|
pub const pathname = bridge.accessor(Location.getPathname, null, .{});
|
||||||
pub const hostname = bridge.accessor(Location.getHostname, null, .{});
|
pub const hostname = bridge.accessor(Location.getHostname, null, .{});
|
||||||
pub const host = bridge.accessor(Location.getHost, null, .{});
|
pub const host = bridge.accessor(Location.getHost, null, .{});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ _search_params: ?*URLSearchParams = null,
|
|||||||
|
|
||||||
// convenience
|
// convenience
|
||||||
pub const resolve = @import("../URL.zig").resolve;
|
pub const resolve = @import("../URL.zig").resolve;
|
||||||
|
pub const eqlDocument = @import("../URL.zig").eqlDocument;
|
||||||
|
|
||||||
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {
|
||||||
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
|
const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const log = @import("../../log.zig");
|
|||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Console = @import("Console.zig");
|
const Console = @import("Console.zig");
|
||||||
const History = @import("History.zig");
|
const History = @import("History.zig");
|
||||||
|
const Navigation = @import("navigation/Navigation.zig");
|
||||||
const Crypto = @import("Crypto.zig");
|
const Crypto = @import("Crypto.zig");
|
||||||
const CSS = @import("CSS.zig");
|
const CSS = @import("CSS.zig");
|
||||||
const Navigator = @import("Navigator.zig");
|
const Navigator = @import("Navigator.zig");
|
||||||
@@ -51,9 +52,10 @@ _console: Console = .init,
|
|||||||
_navigator: Navigator = .init,
|
_navigator: Navigator = .init,
|
||||||
_screen: Screen = .init,
|
_screen: Screen = .init,
|
||||||
_performance: Performance,
|
_performance: Performance,
|
||||||
_history: History,
|
|
||||||
_storage_bucket: *storage.Bucket,
|
_storage_bucket: *storage.Bucket,
|
||||||
_on_load: ?js.Function = null,
|
_on_load: ?js.Function = null,
|
||||||
|
_on_pageshow: ?js.Function = null,
|
||||||
|
_on_popstate: ?js.Function = null,
|
||||||
_on_error: ?js.Function = null, // TODO: invoke on error?
|
_on_error: ?js.Function = null, // TODO: invoke on error?
|
||||||
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
|
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
|
||||||
_location: *Location,
|
_location: *Location,
|
||||||
@@ -113,8 +115,12 @@ pub fn getLocation(self: *const Window) *Location {
|
|||||||
return self._location;
|
return self._location;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getHistory(self: *Window) *History {
|
pub fn getHistory(_: *Window, page: *Page) *History {
|
||||||
return &self._history;
|
return &page._session.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNavigation(_: *Window, page: *Page) *Navigation {
|
||||||
|
return &page._session.navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
|
pub fn getCustomElements(self: *Window) *CustomElementRegistry {
|
||||||
@@ -133,6 +139,30 @@ pub fn setOnLoad(self: *Window, cb_: ?js.Function) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getOnPageShow(self: *const Window) ?js.Function {
|
||||||
|
return self._on_pageshow;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnPageShow(self: *Window, cb_: ?js.Function) !void {
|
||||||
|
if (cb_) |cb| {
|
||||||
|
self._on_pageshow = cb;
|
||||||
|
} else {
|
||||||
|
self._on_pageshow = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnPopState(self: *const Window) ?js.Function {
|
||||||
|
return self._on_popstate;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnPopState(self: *Window, cb_: ?js.Function) !void {
|
||||||
|
if (cb_) |cb| {
|
||||||
|
self._on_popstate = cb;
|
||||||
|
} else {
|
||||||
|
self._on_popstate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getOnError(self: *const Window) ?js.Function {
|
pub fn getOnError(self: *const Window) ?js.Function {
|
||||||
return self._on_error;
|
return self._on_error;
|
||||||
}
|
}
|
||||||
@@ -486,11 +516,14 @@ pub const JsApi = struct {
|
|||||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
|
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
|
||||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
|
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
|
||||||
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
|
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
|
||||||
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
|
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
||||||
|
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
||||||
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
|
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
|
||||||
pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
|
pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
|
||||||
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
|
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
|
||||||
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
|
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
|
||||||
|
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
||||||
|
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||||
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
|
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
|
||||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||||
pub const fetch = bridge.function(Window.fetch, .{});
|
pub const fetch = bridge.function(Window.fetch, .{});
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
const Navigaton = @import("../navigation/Navigation.zig");
|
||||||
|
const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig");
|
||||||
|
const NavigationType = @import("../navigation/root.zig").NavigationType;
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
|
||||||
|
const NavigationCurrentEntryChangeEvent = @This();
|
||||||
|
|
||||||
|
_proto: *Event,
|
||||||
|
_from: *NavigationHistoryEntry,
|
||||||
|
_navigation_type: ?NavigationType,
|
||||||
|
|
||||||
|
pub const EventInit = struct {
|
||||||
|
from: *NavigationHistoryEntry,
|
||||||
|
navigationType: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
typ: []const u8,
|
||||||
|
init_obj: EventInit,
|
||||||
|
page: *Page,
|
||||||
|
) !*NavigationCurrentEntryChangeEvent {
|
||||||
|
const navigation_type = if (init_obj.navigationType) |nav_type_str|
|
||||||
|
std.meta.stringToEnum(NavigationType, nav_type_str)
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
return page._factory.event(typ, NavigationCurrentEntryChangeEvent{
|
||||||
|
._proto = undefined,
|
||||||
|
._from = init_obj.from,
|
||||||
|
._navigation_type = navigation_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getFrom(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {
|
||||||
|
return self._from;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNavigationType(self: *const NavigationCurrentEntryChangeEvent) ?[]const u8 {
|
||||||
|
return if (self._navigation_type) |nav_type| @tagName(nav_type) else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(NavigationCurrentEntryChangeEvent);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "NavigationCurrentEntryChangeEvent";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});
|
||||||
|
pub const from = bridge.accessor(NavigationCurrentEntryChangeEvent.getFrom, null, .{});
|
||||||
|
pub const navigationType = bridge.accessor(NavigationCurrentEntryChangeEvent.getNavigationType, null, .{});
|
||||||
|
};
|
||||||
61
src/browser/webapi/event/PageTransitionEvent.zig
Normal file
61
src/browser/webapi/event/PageTransitionEvent.zig
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const log = @import("../../../log.zig");
|
||||||
|
// const Window = @import("../html/window.zig").Window;
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent
|
||||||
|
const PageTransitionEvent = @This();
|
||||||
|
|
||||||
|
const EventInit = struct {
|
||||||
|
persisted: ?bool = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
_proto: *Event,
|
||||||
|
_persisted: bool,
|
||||||
|
|
||||||
|
pub fn init(typ: []const u8, init_obj: EventInit, page: *Page) !*PageTransitionEvent {
|
||||||
|
return page._factory.event(typ, PageTransitionEvent{
|
||||||
|
._proto = undefined,
|
||||||
|
._persisted = init_obj.persisted orelse false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asEvent(self: *PageTransitionEvent) *Event {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPersisted(self: *PageTransitionEvent) bool {
|
||||||
|
return self._persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PageTransitionEvent);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PageTransitionEvent";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});
|
||||||
|
pub const persisted = bridge.accessor(PageTransitionEvent.getPersisted, null, .{});
|
||||||
|
};
|
||||||
72
src/browser/webapi/event/PopStateEvent.zig
Normal file
72
src/browser/webapi/event/PopStateEvent.zig
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const log = @import("../../../log.zig");
|
||||||
|
// const Window = @import("../html/window.zig").Window;
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
|
||||||
|
const PopStateEvent = @This();
|
||||||
|
|
||||||
|
const EventOptions = struct {
|
||||||
|
state: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
_proto: *Event,
|
||||||
|
_state: ?[]const u8,
|
||||||
|
|
||||||
|
pub fn init(typ: []const u8, _options: ?EventOptions, page: *Page) !*PopStateEvent {
|
||||||
|
const options = _options orelse EventOptions{};
|
||||||
|
|
||||||
|
return page._factory.event(typ, PopStateEvent{
|
||||||
|
._proto = undefined,
|
||||||
|
._state = options.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asEvent(self: *PopStateEvent) *Event {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {
|
||||||
|
if (self._state == null) return null;
|
||||||
|
|
||||||
|
const value = try js.Value.fromJson(page.js, self._state.?);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasUAVisualTransition(_: *PopStateEvent) bool {
|
||||||
|
// Not currently supported so we always return false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PopStateEvent);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PopStateEvent";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(PopStateEvent.init, .{});
|
||||||
|
pub const state = bridge.accessor(PopStateEvent.getState, null, .{});
|
||||||
|
pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.hasUAVisualTransition, null, .{});
|
||||||
|
};
|
||||||
436
src/browser/webapi/navigation/Navigation.zig
Normal file
436
src/browser/webapi/navigation/Navigation.zig
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../../log.zig");
|
||||||
|
const URL = @import("../URL.zig");
|
||||||
|
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
const EventTarget = @import("../EventTarget.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
|
||||||
|
const Navigation = @This();
|
||||||
|
|
||||||
|
const NavigationKind = @import("root.zig").NavigationKind;
|
||||||
|
const NavigationActivation = @import("NavigationActivation.zig");
|
||||||
|
const NavigationTransition = @import("root.zig").NavigationTransition;
|
||||||
|
const NavigationState = @import("root.zig").NavigationState;
|
||||||
|
|
||||||
|
const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig");
|
||||||
|
const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig");
|
||||||
|
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||||
|
|
||||||
|
_proto: *NavigationEventTarget = undefined,
|
||||||
|
_arena: std.mem.Allocator,
|
||||||
|
_current_navigation_kind: ?NavigationKind = null,
|
||||||
|
|
||||||
|
_index: usize = 0,
|
||||||
|
// Need to be stable pointers, because Events can reference entries.
|
||||||
|
_entries: std.ArrayList(*NavigationHistoryEntry) = .empty,
|
||||||
|
_next_entry_id: usize = 0,
|
||||||
|
_activation: ?NavigationActivation = null,
|
||||||
|
|
||||||
|
pub fn init(arena: std.mem.Allocator) Navigation {
|
||||||
|
return Navigation{ ._arena = arena };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asEventTarget(self: *Navigation) *EventTarget {
|
||||||
|
return self._proto.asEventTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onRemovePage(self: *Navigation) void {
|
||||||
|
self._proto = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onNewPage(self: *Navigation, page: *Page) !void {
|
||||||
|
self._proto = try page._factory.eventTarget(
|
||||||
|
NavigationEventTarget{ ._proto = undefined },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getActivation(self: *const Navigation) ?NavigationActivation {
|
||||||
|
return self._activation;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCanGoBack(self: *const Navigation) bool {
|
||||||
|
return self._index > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCanGoForward(self: *const Navigation) bool {
|
||||||
|
return self._entries.items.len > self._index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCurrentEntryOrNull(self: *Navigation) ?*NavigationHistoryEntry {
|
||||||
|
if (self._entries.items.len > self._index) {
|
||||||
|
return self._entries.items[self._index];
|
||||||
|
} else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry {
|
||||||
|
// This should never fail. An entry should always be created before
|
||||||
|
// we run the scripts on the page we are loading.
|
||||||
|
std.debug.assert(self._entries.items.len > 0);
|
||||||
|
|
||||||
|
return self.getCurrentEntryOrNull().?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTransition(_: *const Navigation) ?NavigationTransition {
|
||||||
|
// For now, all transitions are just considered complete.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationReturn = struct {
|
||||||
|
committed: js.Promise,
|
||||||
|
finished: js.Promise,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn back(self: *Navigation, page: *Page) !NavigationReturn {
|
||||||
|
if (!self.getCanGoBack()) {
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_index = self._index - 1;
|
||||||
|
const next_entry = self._entries.items[new_index];
|
||||||
|
|
||||||
|
return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(self: *const Navigation) []*NavigationHistoryEntry {
|
||||||
|
return self._entries.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forward(self: *Navigation, page: *Page) !NavigationReturn {
|
||||||
|
if (!self.getCanGoForward()) {
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_index = self._index + 1;
|
||||||
|
const next_entry = self._entries.items[new_index];
|
||||||
|
|
||||||
|
return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
|
||||||
|
switch (kind) {
|
||||||
|
.replace => |state| {
|
||||||
|
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
|
||||||
|
},
|
||||||
|
.push => |state| {
|
||||||
|
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
|
||||||
|
},
|
||||||
|
.traverse => |index| {
|
||||||
|
self._index = index;
|
||||||
|
},
|
||||||
|
.reload => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is for after true navigation processing, where we need to ensure that our entries are up to date.
|
||||||
|
//
|
||||||
|
// This is only really safe to run in the `pageDoneCallback`
|
||||||
|
// where we can guarantee that the URL and NavigationKind are correct.
|
||||||
|
pub fn commitNavigation(self: *Navigation, page: *Page) !void {
|
||||||
|
const url = page.url;
|
||||||
|
|
||||||
|
const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null };
|
||||||
|
defer self._current_navigation_kind = null;
|
||||||
|
|
||||||
|
const from_entry = self.getCurrentEntryOrNull();
|
||||||
|
try self.updateEntries(url, kind, page, false);
|
||||||
|
|
||||||
|
self._activation = NavigationActivation{
|
||||||
|
._from = from_entry,
|
||||||
|
._entry = self.getCurrentEntry(),
|
||||||
|
._type = kind.toNavigationType(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
|
||||||
|
/// For that, use `navigate`.
|
||||||
|
pub fn pushEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: [:0]const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
|
const arena = self._arena;
|
||||||
|
const url = try arena.dupeZ(u8, _url);
|
||||||
|
|
||||||
|
// truncates our history here.
|
||||||
|
if (self._entries.items.len > self._index + 1) {
|
||||||
|
self._entries.shrinkRetainingCapacity(self._index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = self._entries.items.len;
|
||||||
|
|
||||||
|
const id = self._next_entry_id;
|
||||||
|
self._next_entry_id += 1;
|
||||||
|
|
||||||
|
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
|
||||||
|
|
||||||
|
const entry = try arena.create(NavigationHistoryEntry);
|
||||||
|
entry.* = NavigationHistoryEntry{
|
||||||
|
._id = id_str,
|
||||||
|
._key = id_str,
|
||||||
|
._url = url,
|
||||||
|
._state = state,
|
||||||
|
};
|
||||||
|
|
||||||
|
// we don't always have a current entry...
|
||||||
|
const previous = if (self._entries.items.len > 0) self.getCurrentEntry() else null;
|
||||||
|
try self._entries.append(arena, entry);
|
||||||
|
self._index = index;
|
||||||
|
|
||||||
|
if (previous) |prev| {
|
||||||
|
if (dispatch) {
|
||||||
|
const event = try NavigationCurrentEntryChangeEvent.init(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = prev, .navigationType = @tagName(.push) },
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
try self._proto.dispatch(.{ .currententrychange = event }, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replaceEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: [:0]const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
|
const arena = self._arena;
|
||||||
|
const url = try arena.dupeZ(u8, _url);
|
||||||
|
|
||||||
|
const previous = self.getCurrentEntry();
|
||||||
|
|
||||||
|
const id = self._next_entry_id;
|
||||||
|
self._next_entry_id += 1;
|
||||||
|
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
|
||||||
|
|
||||||
|
const entry = try arena.create(NavigationHistoryEntry);
|
||||||
|
entry.* = NavigationHistoryEntry{
|
||||||
|
._id = id_str,
|
||||||
|
._key = previous._key,
|
||||||
|
._url = url,
|
||||||
|
._state = state,
|
||||||
|
};
|
||||||
|
|
||||||
|
self._entries.items[self._index] = entry;
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
|
const event = try NavigationCurrentEntryChangeEvent.init(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = previous, .navigationType = @tagName(.replace) },
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
try self._proto.dispatch(.{ .currententrychange = event }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigateOptions = struct {
|
||||||
|
state: ?js.Object = null,
|
||||||
|
info: ?js.Object = null,
|
||||||
|
history: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn navigateInner(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: ?[:0]const u8,
|
||||||
|
kind: NavigationKind,
|
||||||
|
page: *Page,
|
||||||
|
) !NavigationReturn {
|
||||||
|
const arena = self._arena;
|
||||||
|
const url = _url orelse return error.MissingURL;
|
||||||
|
|
||||||
|
// https://github.com/WICG/navigation-api/issues/95
|
||||||
|
//
|
||||||
|
// These will only settle on same-origin navigation (mostly intended for SPAs).
|
||||||
|
// It is fine (and expected) for these to not settle on cross-origin requests :)
|
||||||
|
const committed = try page.js.createPromiseResolver(.page);
|
||||||
|
const finished = try page.js.createPromiseResolver(.page);
|
||||||
|
|
||||||
|
const new_url = try URL.resolve(arena, page.url, url, .{});
|
||||||
|
const is_same_document = URL.eqlDocument(new_url, page.url);
|
||||||
|
|
||||||
|
const previous = self.getCurrentEntry();
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
.push => |state| {
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
committed.resolve("navigation push", {});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
finished.resolve("navigation push", {});
|
||||||
|
|
||||||
|
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||||
|
} else {
|
||||||
|
try page.navigate(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.replace => |state| {
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
committed.resolve("navigation replace", {});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
finished.resolve("navigation replace", {});
|
||||||
|
|
||||||
|
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||||
|
} else {
|
||||||
|
try page.navigate(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.traverse => |index| {
|
||||||
|
self._index = index;
|
||||||
|
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
committed.resolve("navigation traverse", {});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
finished.resolve("navigation traverse", {});
|
||||||
|
} else {
|
||||||
|
try page.navigate(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.reload => {
|
||||||
|
try page.navigate(url, .{ .reason = .navigation }, kind);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't navigated off, let us fire off an a currententrychange.
|
||||||
|
const event = try NavigationCurrentEntryChangeEvent.init(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = previous, .navigationType = @tagName(kind) },
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
try self._proto.dispatch(.{ .currententrychange = event }, page);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.committed = committed.promise(),
|
||||||
|
.finished = finished.promise(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate(self: *Navigation, _url: [:0]const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
|
||||||
|
const opts = _opts orelse NavigateOptions{};
|
||||||
|
const json = if (opts.state) |state| state.toJson(self._arena) catch return error.DataClone else null;
|
||||||
|
|
||||||
|
const kind: NavigationKind = if (opts.history) |history|
|
||||||
|
if (std.mem.eql(u8, "replace", history)) .{ .replace = json } else .{ .push = json }
|
||||||
|
else
|
||||||
|
.{ .push = json };
|
||||||
|
|
||||||
|
return try self.navigateInner(_url, kind, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ReloadOptions = struct {
|
||||||
|
state: ?js.Object = null,
|
||||||
|
info: ?js.Object = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {
|
||||||
|
const arena = self._arena;
|
||||||
|
|
||||||
|
const opts = _opts orelse ReloadOptions{};
|
||||||
|
const entry = self.getCurrentEntry();
|
||||||
|
if (opts.state) |state| {
|
||||||
|
const previous = entry;
|
||||||
|
entry._state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
|
||||||
|
|
||||||
|
const event = try NavigationCurrentEntryChangeEvent.init(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = previous, .navigationType = @tagName(.reload) },
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
try self._proto.dispatch(.{ .currententrychange = event }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.navigateInner(entry._url, .reload, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TraverseToOptions = struct {
|
||||||
|
info: ?js.Object = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {
|
||||||
|
if (_opts != null) {
|
||||||
|
log.debug(.browser, "not implemented", .{ .options = _opts });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self._entries.items, 0..) |entry, i| {
|
||||||
|
if (std.mem.eql(u8, key, entry._key)) {
|
||||||
|
return try self.navigateInner(entry._url, .{ .traverse = i }, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const UpdateCurrentEntryOptions = struct {
|
||||||
|
state: js.Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {
|
||||||
|
const arena = self._arena;
|
||||||
|
|
||||||
|
const previous = self.getCurrentEntry();
|
||||||
|
self.getCurrentEntry()._state = .{
|
||||||
|
.source = .navigation,
|
||||||
|
.value = options.state.toJson(arena) catch return error.DataClone,
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = try NavigationCurrentEntryChangeEvent.init(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = previous, .navigationType = null },
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
try self._proto.dispatch(.{ .currententrychange = event }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(Navigation);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "Navigation";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const activation = bridge.accessor(Navigation.getActivation, null, .{});
|
||||||
|
pub const canGoBack = bridge.accessor(Navigation.getCanGoBack, null, .{});
|
||||||
|
pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{});
|
||||||
|
pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{});
|
||||||
|
pub const transition = bridge.accessor(Navigation.getTransition, null, .{});
|
||||||
|
pub const back = bridge.function(Navigation.back, .{});
|
||||||
|
pub const entries = bridge.function(Navigation.entries, .{});
|
||||||
|
pub const forward = bridge.function(Navigation.forward, .{});
|
||||||
|
pub const navigate = bridge.function(Navigation.navigate, .{});
|
||||||
|
pub const traverseTo = bridge.function(Navigation.traverseTo, .{});
|
||||||
|
pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{});
|
||||||
|
};
|
||||||
56
src/browser/webapi/navigation/NavigationActivation.zig
Normal file
56
src/browser/webapi/navigation/NavigationActivation.zig
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
|
||||||
|
const NavigationType = @import("root.zig").NavigationType;
|
||||||
|
const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
|
||||||
|
const NavigationActivation = @This();
|
||||||
|
|
||||||
|
_entry: *NavigationHistoryEntry,
|
||||||
|
_from: ?*NavigationHistoryEntry = null,
|
||||||
|
_type: NavigationType,
|
||||||
|
|
||||||
|
pub fn getEntry(self: *const NavigationActivation) *NavigationHistoryEntry {
|
||||||
|
return self._entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getFrom(self: *const NavigationActivation) ?*NavigationHistoryEntry {
|
||||||
|
return self._from;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNavigationType(self: *const NavigationActivation) []const u8 {
|
||||||
|
return @tagName(self._type);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(NavigationActivation);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "NavigationActivation";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const entry = bridge.accessor(NavigationActivation.getEntry, null, .{});
|
||||||
|
pub const from = bridge.accessor(NavigationActivation.getFrom, null, .{});
|
||||||
|
pub const navigationType = bridge.accessor(NavigationActivation.getNavigationType, null, .{});
|
||||||
|
};
|
||||||
80
src/browser/webapi/navigation/NavigationEventTarget.zig
Normal file
80
src/browser/webapi/navigation/NavigationEventTarget.zig
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const EventTarget = @import("../EventTarget.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig");
|
||||||
|
|
||||||
|
pub const NavigationEventTarget = @This();
|
||||||
|
|
||||||
|
_proto: *EventTarget,
|
||||||
|
_on_currententrychange: ?js.Function = null,
|
||||||
|
|
||||||
|
pub fn asEventTarget(self: *NavigationEventTarget) *EventTarget {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DispatchType = union(enum) {
|
||||||
|
currententrychange: *NavigationCurrentEntryChangeEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *Page) !void {
|
||||||
|
const event, const field = blk: {
|
||||||
|
break :blk switch (event_type) {
|
||||||
|
.currententrychange => |cec| .{ cec.asEvent(), "_on_currententrychange" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return page._event_manager.dispatchWithFunction(
|
||||||
|
self.asEventTarget(),
|
||||||
|
event,
|
||||||
|
@field(self, field),
|
||||||
|
.{ .context = "Navigation" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnCurrentEntryChange(self: *NavigationEventTarget) ?js.Function {
|
||||||
|
return self._on_currententrychange;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnCurrentEntryChange(self: *NavigationEventTarget, listener: ?js.Function) !void {
|
||||||
|
if (listener) |listen| {
|
||||||
|
self._on_currententrychange = try listen.withThis(self);
|
||||||
|
} else {
|
||||||
|
self._on_currententrychange = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(NavigationEventTarget);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "NavigationEventTarget";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const oncurrententrychange = bridge.accessor(
|
||||||
|
NavigationEventTarget.getOnCurrentEntryChange,
|
||||||
|
NavigationEventTarget.setOnCurrentEntryChange,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
};
|
||||||
106
src/browser/webapi/navigation/NavigationHistoryEntry.zig
Normal file
106
src/browser/webapi/navigation/NavigationHistoryEntry.zig
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const URL = @import("../URL.zig");
|
||||||
|
const EventTarget = @import("../EventTarget.zig");
|
||||||
|
const NavigationState = @import("root.zig").NavigationState;
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
|
||||||
|
const NavigationHistoryEntry = @This();
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
|
||||||
|
// no proto for now
|
||||||
|
// _proto: ?*EventTarget,
|
||||||
|
_id: []const u8,
|
||||||
|
_key: []const u8,
|
||||||
|
_url: ?[:0]const u8,
|
||||||
|
_state: NavigationState,
|
||||||
|
|
||||||
|
// fn asEventTarget(self: *NavigationHistoryEntry) *EventTarget {
|
||||||
|
// return self._proto.?.asEventTarget();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn onRemovePage(self: *NavigationHistoryEntry) void {
|
||||||
|
// self._proto = null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn onNewPage(self: *NavigationHistoryEntry, page: *Page) !void {
|
||||||
|
// self._proto = try page._factory.eventTarget(
|
||||||
|
// NavigationHistoryEntryEventTarget{ ._proto = undefined },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn id(self: *const NavigationHistoryEntry) []const u8 {
|
||||||
|
return self._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(self: *const NavigationHistoryEntry, page: *Page) i32 {
|
||||||
|
const navigation = &page._session.navigation;
|
||||||
|
|
||||||
|
for (navigation._entries.items, 0..) |entry, i| {
|
||||||
|
if (std.mem.eql(u8, entry._id, self._id)) {
|
||||||
|
return @intCast(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(self: *const NavigationHistoryEntry) []const u8 {
|
||||||
|
return self._key;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool {
|
||||||
|
const got_url = self._url orelse return false;
|
||||||
|
return URL.eqlDocument(got_url, page.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(self: *const NavigationHistoryEntry) ?[:0]const u8 {
|
||||||
|
return self._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
|
||||||
|
|
||||||
|
pub fn getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
|
||||||
|
if (self._state.source == .navigation) {
|
||||||
|
if (self._state.value) |value| {
|
||||||
|
return .{ .value = try js.Value.fromJson(page.js, value) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(NavigationHistoryEntry);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "NavigationHistoryEntry";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const id = bridge.accessor(NavigationHistoryEntry.id, null, .{});
|
||||||
|
pub const index = bridge.accessor(NavigationHistoryEntry.index, null, .{});
|
||||||
|
pub const key = bridge.accessor(NavigationHistoryEntry.key, null, .{});
|
||||||
|
pub const sameDocument = bridge.accessor(NavigationHistoryEntry.sameDocument, null, .{});
|
||||||
|
pub const url = bridge.accessor(NavigationHistoryEntry.url, null, .{});
|
||||||
|
pub const getState = bridge.function(NavigationHistoryEntry.getState, .{});
|
||||||
|
};
|
||||||
57
src/browser/webapi/navigation/root.zig
Normal file
57
src/browser/webapi/navigation/root.zig
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../../log.zig");
|
||||||
|
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
const Navigation = @import("Navigation.zig");
|
||||||
|
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||||
|
const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig");
|
||||||
|
|
||||||
|
pub const NavigationType = enum {
|
||||||
|
push,
|
||||||
|
replace,
|
||||||
|
traverse,
|
||||||
|
reload,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NavigationKind = union(NavigationType) {
|
||||||
|
push: ?[]const u8,
|
||||||
|
replace: ?[]const u8,
|
||||||
|
traverse: usize,
|
||||||
|
reload,
|
||||||
|
|
||||||
|
pub fn toNavigationType(self: NavigationKind) NavigationType {
|
||||||
|
return std.meta.activeTag(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NavigationState = struct {
|
||||||
|
source: enum { history, navigation },
|
||||||
|
value: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
|
||||||
|
pub const NavigationTransition = struct {
|
||||||
|
finished: js.Promise,
|
||||||
|
from: NavigationHistoryEntry,
|
||||||
|
navigation_type: NavigationType,
|
||||||
|
};
|
||||||
@@ -183,7 +183,7 @@ fn navigate(cmd: anytype) !void {
|
|||||||
try page.navigate(params.url, .{
|
try page.navigate(params.url, .{
|
||||||
.reason = .address_bar,
|
.reason = .address_bar,
|
||||||
.cdp_id = cmd.input.id,
|
.cdp_id = cmd.input.id,
|
||||||
});
|
}, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void {
|
pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void {
|
||||||
@@ -206,7 +206,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
.POST => "formSubmissionPost",
|
.POST => "formSubmissionPost",
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
},
|
},
|
||||||
.address_bar => null,
|
.address_bar, .navigation => null,
|
||||||
};
|
};
|
||||||
if (reason_) |reason| {
|
if (reason_) |reason| {
|
||||||
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||||
|
|||||||
@@ -179,9 +179,11 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
try doAttachtoTarget(cmd, target_id);
|
try doAttachtoTarget(cmd, target_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
try page.navigate(params.url, .{
|
try page.navigate(
|
||||||
.reason = .address_bar,
|
params.url,
|
||||||
});
|
.{ .reason = .address_bar },
|
||||||
|
.{ .push = null },
|
||||||
|
);
|
||||||
|
|
||||||
try cmd.sendResult(.{
|
try cmd.sendResult(.{
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const TestContext = struct {
|
|||||||
.{url},
|
.{url},
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
try page.navigate(full_url, .{});
|
try page.navigate(full_url, .{}, .{ .push = null });
|
||||||
bc.session.fetchWait(2000);
|
bc.session.fetchWait(2000);
|
||||||
}
|
}
|
||||||
return bc;
|
return bc;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
_ = try page.navigate(url, .{});
|
_ = try page.navigate(url, .{}, .{ .push = null });
|
||||||
_ = session.fetchWait(opts.wait_ms);
|
_ = session.fetchWait(opts.wait_ms);
|
||||||
|
|
||||||
const writer = opts.writer orelse return;
|
const writer = opts.writer orelse return;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
|
|||||||
try_catch.init(js_context);
|
try_catch.init(js_context);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{}, .{ .push = null });
|
||||||
session.fetchWait(2000);
|
session.fetchWait(2000);
|
||||||
|
|
||||||
page._session.browser.runMicrotasks();
|
page._session.browser.runMicrotasks();
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ fn run(
|
|||||||
defer session.removePage();
|
defer session.removePage();
|
||||||
|
|
||||||
const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0);
|
const url = try std.fmt.allocPrintSentinel(arena, "http://localhost:9582/{s}", .{test_file}, 0);
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{}, .{ .push = null });
|
||||||
|
|
||||||
_ = page.wait(2000);
|
_ = page.wait(2000);
|
||||||
|
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
|
|||||||
try_catch.init(js_context);
|
try_catch.init(js_context);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{}, .{ .push = null });
|
||||||
test_session.fetchWait(2000);
|
test_session.fetchWait(2000);
|
||||||
|
|
||||||
page._session.browser.runMicrotasks();
|
page._session.browser.runMicrotasks();
|
||||||
@@ -428,7 +428,7 @@ pub fn pageTest(comptime test_file: []const u8) !*Page {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{}, .{ .push = null });
|
||||||
test_session.fetchWait(2000);
|
test_session.fetchWait(2000);
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule tests/wpt updated: d9978ebd7b...3df84d931c
Reference in New Issue
Block a user