diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig
index d67da3f5..5ef5ac3a 100644
--- a/src/browser/html/History.zig
+++ b/src/browser/html/History.zig
@@ -81,7 +81,9 @@ fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
- _ = try page.session.navigation.pushEntry(url, .{ .state = state }, page);
+
+ const json = state.toJson(arena) catch return error.DataClone;
+ _ = try page.session.navigation.pushEntry(url, json, page);
}
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
@@ -113,7 +115,7 @@ pub fn go(_: *const History, delta: i32, page: *Page) !void {
}
}
- _ = try entry.navigate(page, .force);
+ _ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
}
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
diff --git a/src/browser/html/Navigation.zig b/src/browser/html/Navigation.zig
index 7968d821..c7eb9276 100644
--- a/src/browser/html/Navigation.zig
+++ b/src/browser/html/Navigation.zig
@@ -34,16 +34,24 @@ const Navigation = @This();
pub const Interfaces = .{
Navigation,
NavigationActivation,
+ NavigationTransition,
NavigationHistoryEntry,
};
+pub const NavigationKind = union(enum) {
+ initial,
+ push: ?[]const u8,
+ replace,
+ traverse: usize,
+ reload,
+};
+
pub const prototype = *EventTarget;
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
index: usize = 0,
entries: std.ArrayListUnmanaged(NavigationHistoryEntry) = .empty,
next_entry_id: usize = 0,
-// TODO: key->index mapping
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
const NavigationHistoryEntry = struct {
@@ -91,49 +99,17 @@ const NavigationHistoryEntry = struct {
return null;
}
}
-
- pub fn navigate(entry: NavigationHistoryEntry, reload: enum { none, force }, page: *Page) !NavigationReturn {
- const arena = page.session.arena;
- const url = entry.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.parse(url, null);
- if (try page.url.eqlDocument(&new_url, arena) or reload == .force) {
- page.url = new_url;
- try committed.resolve({});
-
- // todo: Fire navigate event
-
- try finished.resolve({});
- } else {
- // TODO: Change to history
- try page.navigateFromWebAPI(url, .{ .reason = .history });
- }
-
- return .{
- .committed = committed.promise(),
- .finished = finished.promise(),
- };
- }
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
const NavigationActivation = struct {
const NavigationActivationType = enum {
+ pub const ENUM_JS_USE_TAG = true;
+
push,
reload,
replace,
traverse,
-
- pub fn toString(self: NavigationActivationType) []const u8 {
- return @tagName(self);
- }
};
entry: NavigationHistoryEntry,
@@ -153,6 +129,13 @@ const NavigationActivation = struct {
}
};
+// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
+const NavigationTransition = struct {
+ finished: js.Promise,
+ from: NavigationHistoryEntry,
+ navigation_type: NavigationActivation.NavigationActivationType,
+};
+
pub fn get_canGoBack(self: *const Navigation) bool {
return self.index > 0;
}
@@ -169,6 +152,11 @@ pub fn get_currentEntry(self: *const Navigation) NavigationHistoryEntry {
return self.entries.items[self.index];
}
+pub fn get_transition(_: *const Navigation) ?NavigationTransition {
+ // For now, all transitions are just considered complete.
+ return null;
+}
+
const NavigationReturn = struct {
committed: js.Promise,
finished: js.Promise,
@@ -183,7 +171,7 @@ pub fn _back(self: *Navigation, page: *Page) !NavigationReturn {
const next_entry = self.entries.items[new_index];
self.index = new_index;
- return next_entry.navigate(.none, page);
+ return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
pub fn _entries(self: *const Navigation) []NavigationHistoryEntry {
@@ -199,15 +187,33 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
const next_entry = self.entries.items[new_index];
self.index = new_index;
- return next_entry.navigate(.none, page);
+ return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
+}
+
+// This is for after true navigation processing, where we need to ensure that our entries are up to date.
+pub fn processNavigation(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page) !void {
+ switch (kind) {
+ .initial => {
+ _ = try self.pushEntry(url, null, page);
+ },
+ .replace => {
+ // When replacing, we just update the URL but the state is nullified.
+ const entry = self.currentEntry();
+ entry.url = url;
+ entry.state = null;
+ },
+ .push => |state| {
+ _ = try self.pushEntry(url, state, page);
+ },
+ .traverse, .reload => {},
+ }
}
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
/// For that, use `navigate`.
-pub fn pushEntry(self: *Navigation, _url: ?[]const u8, _opts: ?NavigateOptions, page: *Page) !NavigationHistoryEntry {
+pub fn pushEntry(self: *Navigation, _url: ?[]const u8, state: ?[]const u8, page: *Page) !NavigationHistoryEntry {
const arena = page.session.arena;
- const options = _opts orelse NavigateOptions{};
const url = if (_url) |u| try arena.dupe(u8, u) else null;
// truncates our history here.
@@ -221,14 +227,6 @@ pub fn pushEntry(self: *Navigation, _url: ?[]const u8, _opts: ?NavigateOptions,
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
- const state: ?[]const u8 = blk: {
- if (options.state) |s| {
- break :blk s.toJson(arena) catch return error.DataClone;
- } else {
- break :blk null;
- }
- };
-
const entry = NavigationHistoryEntry{
.id = id_str,
.key = id_str,
@@ -237,7 +235,6 @@ pub fn pushEntry(self: *Navigation, _url: ?[]const u8, _opts: ?NavigateOptions,
};
try self.entries.append(arena, entry);
-
return entry;
}
@@ -255,9 +252,67 @@ const NavigateOptions = struct {
history: NavigateOptionsHistory = .auto,
};
+pub fn navigate(
+ self: *Navigation,
+ _url: ?[]const u8,
+ kind: NavigationKind,
+ page: *Page,
+) !NavigationReturn {
+ const arena = page.session.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.parse(url, null);
+ const is_same_document = try page.url.eqlDocument(&new_url, arena);
+
+ switch (kind) {
+ .push => |state| {
+ if (is_same_document) {
+ page.url = new_url;
+ try committed.resolve({});
+ // todo: Fire navigate event
+ try finished.resolve({});
+
+ _ = try self.pushEntry(url, state, page);
+ } else {
+ try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
+ }
+ },
+ .traverse => |index| {
+ self.index = index;
+
+ if (is_same_document) {
+ page.url = new_url;
+
+ try committed.resolve({});
+ // todo: Fire navigate event
+ try finished.resolve({});
+ } else {
+ try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
+ }
+ },
+ .reload => {
+ try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
+ },
+ else => unreachable,
+ }
+
+ return .{
+ .committed = committed.promise(),
+ .finished = finished.promise(),
+ };
+}
+
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
- const entry = try self.pushEntry(_url, _opts, page);
- return entry.navigate(.none, page);
+ const opts = _opts orelse NavigateOptions{};
+ const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
+ return try self.navigate(_url, .{ .push = json }, page);
}
pub const ReloadOptions = struct {
@@ -274,15 +329,23 @@ pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigatio
entry.state = state.toJson(arena) catch return error.DataClone;
}
- return entry.navigate(.force, page);
+ return self.navigate(entry.url, .reload, page);
}
-pub fn _transition(_: *const Navigation) !NavigationReturn {
- unreachable;
-}
+pub const TraverseToOptions = struct {
+ info: ?js.Object = null,
+};
-pub fn _traverseTo(_: *const Navigation, _: []const u8) !NavigationReturn {
- unreachable;
+pub fn _traverseTo(self: *Navigation, key: []const u8, _: ?TraverseToOptions, page: *Page) !NavigationReturn {
+ // const opts = _opts orelse TraverseToOptions{};
+
+ for (self.entries.items, 0..) |entry, i| {
+ if (std.mem.eql(u8, key, entry.key)) {
+ return try self.navigate(entry.url, .{ .traverse = i }, page);
+ }
+ }
+
+ return error.InvalidStateError;
}
pub const UpdateCurrentEntryOptions = struct {
diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig
index 4020b498..d0fee6d2 100644
--- a/src/browser/html/document.zig
+++ b/src/browser/html/document.zig
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
}
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
- return page.navigateFromWebAPI(url, .{ .reason = .script });
+ return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig
index 3e3e593b..220b4974 100644
--- a/src/browser/html/location.zig
+++ b/src/browser/html/location.zig
@@ -74,15 +74,15 @@ pub const Location = struct {
}
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
- return page.navigateFromWebAPI(url, .{ .reason = .script });
+ return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
- return page.navigateFromWebAPI(url, .{ .reason = .script });
+ return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
}
pub fn _reload(_: *const Location, page: *Page) !void {
- return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
+ return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
}
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig
index e6ea1963..3947c526 100644
--- a/src/browser/html/window.zig
+++ b/src/browser/html/window.zig
@@ -143,7 +143,7 @@ pub const Window = struct {
}
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
- return page.navigateFromWebAPI(url, .{ .reason = .script });
+ return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
// frames return the window itself, but accessing it via a pseudo
diff --git a/src/browser/page.zig b/src/browser/page.zig
index f6283f07..15caacad 100644
--- a/src/browser/page.zig
+++ b/src/browser/page.zig
@@ -34,6 +34,7 @@ const Http = @import("../http/Http.zig");
const ScriptManager = @import("ScriptManager.zig");
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument;
+const NavigationKind = @import("html/Navigation.zig").NavigationKind;
const js = @import("js/js.zig");
const URL = @import("../url.zig").URL;
@@ -815,8 +816,8 @@ pub const Page = struct {
},
}
- // Push the navigation after a successful load.
- _ = try self.session.navigation.pushEntry(self.url.raw, null, self);
+ // We need to handle different navigation types differently.
+ try self.session.navigation.processNavigation(self.url.raw, self.session.navigation_kind, self);
}
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -906,7 +907,7 @@ pub const Page = struct {
.a => {
const element: *parser.Element = @ptrCast(node);
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
- try self.navigateFromWebAPI(href, .{});
+ try self.navigateFromWebAPI(href, .{}, .{ .push = null });
},
.input => {
const element: *parser.Element = @ptrCast(node);
@@ -1043,7 +1044,7 @@ pub const Page = struct {
// As such we schedule the function to be called as soon as possible.
// The page.arena is safe to use here, but the transfer_arena exists
// specifically for this type of lifetime.
- pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
+ pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
const session = self.session;
if (session.queued_navigation != null) {
// It might seem like this should never happen. And it might not,
@@ -1070,6 +1071,8 @@ pub const Page = struct {
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }),
};
+ session.navigation_kind = kind;
+
self.http_client.abort();
// In v8, this throws an exception which JS code cannot catch.
@@ -1120,7 +1123,7 @@ pub const Page = struct {
} else {
action = try URL.concatQueryString(transfer_arena, action, buf.items);
}
- try self.navigateFromWebAPI(action, opts);
+ try self.navigateFromWebAPI(action, opts, .{ .push = null });
}
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
@@ -1178,6 +1181,7 @@ pub const NavigateReason = enum {
form,
script,
history,
+ navigation,
};
pub const NavigateOpts = struct {
diff --git a/src/browser/session.zig b/src/browser/session.zig
index 3456f159..d491cf27 100644
--- a/src/browser/session.zig
+++ b/src/browser/session.zig
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const js = @import("js/js.zig");
const Page = @import("page.zig").Page;
+const NavigationKind = @import("html/Navigation.zig").NavigationKind;
const Browser = @import("browser.zig").Browser;
const NavigateOpts = @import("page.zig").NavigateOpts;
const History = @import("html/History.zig");
@@ -59,6 +60,7 @@ pub const Session = struct {
// https://developer.mozilla.org/en-US/docs/Web/API/History
history: History = .{},
navigation: Navigation = .{},
+ navigation_kind: NavigationKind = .initial,
page: ?Page = null,
diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig
index 1f6b720a..c7bfcfb9 100644
--- a/src/cdp/domains/page.zig
+++ b/src/cdp/domains/page.zig
@@ -174,7 +174,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
var cdp = bc.cdp;
const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick",
- .script, .history => "scriptInitiated",
+ .script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) {
.GET => "formSubmissionGet",
.POST => "formSubmissionPost",
diff --git a/src/testing.zig b/src/testing.zig
index 93c1abad..92ae34f0 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -402,19 +402,13 @@ pub fn htmlRunner(file: []const u8) !void {
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
try page.navigate(url, .{});
- _ = page.wait(2000);
+ test_session.fetchWait(2000);
+
// page exits more aggressively in tests. We want to make sure this is called
// at lease once.
page.session.browser.runMicrotasks();
page.session.browser.runMessageLoop();
- const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait");
- if (needs_second_wait.value.toBool(page.js.isolate)) {
- // sets the isSecondWait flag in testing.
- _ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {};
- _ = page.wait(2000);
- }
-
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {
diff --git a/src/tests/html/history.html b/src/tests/html/history.html
index 0f4ff95f..60b54b52 100644
--- a/src/tests/html/history.html
+++ b/src/tests/html/history.html
@@ -11,10 +11,12 @@
testing.expectEqual('auto', history.scrollRestoration);
testing.expectEqual(null, history.state)
- history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json');
+ history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history2.html');
testing.expectEqual({ testInProgress: true }, history.state);
+ history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
history.replaceState({ "new": "field", testComplete: true }, null);
+
let state = { "new": "field", testComplete: true };
testing.expectEqual(state, history.state);
@@ -31,10 +33,5 @@
testing.expectEqual(state, popstateEventState);
})
- testing.onPageWait(() => {
- testing.expectEqual(true, history.state && history.state.testComplete);
- testing.expectEqual(state, history.state);
- });
-
- testing.expectEqual(undefined, history.go());
+ history.back();
diff --git a/src/tests/html/history2.html b/src/tests/html/history2.html
new file mode 100644
index 00000000..735c71e9
--- /dev/null
+++ b/src/tests/html/history2.html
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/tests/html/navigation.html b/src/tests/html/navigation.html
index f1ff61fb..e3744f55 100644
--- a/src/tests/html/navigation.html
+++ b/src/tests/html/navigation.html
@@ -1,5 +1,6 @@
+
diff --git a/src/tests/html/navigation2.html b/src/tests/html/navigation2.html
new file mode 100644
index 00000000..3b8ad282
--- /dev/null
+++ b/src/tests/html/navigation2.html
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/src/tests/testing.js b/src/tests/testing.js
index 779cca8c..e83744ee 100644
--- a/src/tests/testing.js
+++ b/src/tests/testing.js
@@ -51,14 +51,6 @@
// if we're already in a fail state, return fail, nothing can recover this
if (testing._status === 'fail') return 'fail';
- if (testing._isSecondWait) {
- for (const pw of (testing._onPageWait)) {
- testing._captured = pw[1];
- pw[0]();
- testing._captured = null;
- }
- }
-
// run any eventually's that we've captured
for (const ev of testing._eventually) {
testing._captured = ev[1];
@@ -101,18 +93,6 @@
_registerErrorCallback();
}
- // Set expectations to happen on the next time that `page.wait` is executed.
- //
- // History specifically uses this as it queues navigation that needs to be checked
- // when the next page is loaded.
- function onPageWait(fn) {
- // Store callbacks to run when page.wait() happens
- testing._onPageWait.push([fn, {
- script_id: document.currentScript.id,
- stack: new Error().stack,
- }]);
- }
-
async function async(promise, cb) {
const script_id = document.currentScript ? document.currentScript.id : '