Merge pull request #840 from lightpanda-io/xhr_readystatechange
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled

Add readystate change event to XHR
This commit is contained in:
Karl Seguin
2025-07-06 08:59:19 +08:00
committed by GitHub
6 changed files with 75 additions and 13 deletions

View File

@@ -23,10 +23,12 @@ const Page = @import("../page.zig").Page;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig"); const nod = @import("node.zig");
// EventTarget interfaces pub const Union = union(enum) {
pub const Union = Nod.Union; node: nod.Union,
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
};
// EventTarget implementation // EventTarget implementation
pub const EventTarget = struct { pub const EventTarget = struct {
@@ -39,18 +41,22 @@ pub const EventTarget = struct {
// The window is a common non-node target, but it's easy to handle as // The window is a common non-node target, but it's easy to handle as
// its a singleton. // its a singleton.
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) { if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
return .{ .Window = &page.window }; return .{ .node = .{ .Window = &page.window } };
} }
// AbortSignal is another non-node target. It has a distinct usage though // AbortSignal is another non-node target. It has a distinct usage though
// so we hijack the event internal type to identity if. // so we hijack the event internal type to identity if.
switch (try parser.eventGetInternalType(e)) { switch (try parser.eventGetInternalType(e)) {
.abort_signal => { .abort_signal => {
return .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) }; return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
},
.xhr_event => {
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .xhr = @fieldParentPtr("proto", base) };
}, },
else => { else => {
// some of these probably need to be special-cased like abort_signal return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
}, },
} }
} }

View File

@@ -54,7 +54,7 @@ pub const Event = struct {
pub fn toInterface(evt: *parser.Event) !Union { pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) { return switch (try parser.eventGetInternalType(evt)) {
.event, .abort_signal => .{ .Event = evt }, .event, .abort_signal, .xhr_event => .{ .Event = evt },
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },

View File

@@ -527,6 +527,7 @@ pub const EventType = enum(u8) {
mouse_event = 3, mouse_event = 3,
error_event = 4, error_event = 4,
abort_signal = 5, abort_signal = 5,
xhr_event = 6,
}; };
pub const MutationEvent = c.dom_mutation_event; pub const MutationEvent = c.dom_mutation_event;

View File

@@ -1017,7 +1017,7 @@ const Script = struct {
const src: []const u8 = blk: { const src: []const u8 = blk: {
const s = self.src orelse break :blk page.url.raw; const s = self.src orelse break :blk page.url.raw;
break :blk try URL.stitch(page.arena, s, page.url.raw, .{.alloc = .if_needed}); break :blk try URL.stitch(page.arena, s, page.url.raw, .{ .alloc = .if_needed });
}; };
// if self.src is null, then this is an inline script, and it should // if self.src is null, then this is an inline script, and it should

View File

@@ -39,6 +39,7 @@ pub const XMLHttpRequestEventTarget = struct {
onload_cbk: ?Function = null, onload_cbk: ?Function = null,
ontimeout_cbk: ?Function = null, ontimeout_cbk: ?Function = null,
onloadend_cbk: ?Function = null, onloadend_cbk: ?Function = null,
onreadystatechange_cbk: ?Function = null,
fn register( fn register(
self: *XMLHttpRequestEventTarget, self: *XMLHttpRequestEventTarget,
@@ -86,6 +87,9 @@ pub const XMLHttpRequestEventTarget = struct {
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function { pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function {
return self.onloadend_cbk; return self.onloadend_cbk;
} }
pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?Function {
return self.onreadystatechange_cbk;
}
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, page: *Page) !void { pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, page: *Page) !void {
if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id); if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
@@ -111,4 +115,8 @@ pub const XMLHttpRequestEventTarget = struct {
if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id); if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
self.onloadend_cbk = try self.register(page.arena, "loadend", listener); self.onloadend_cbk = try self.register(page.arena, "loadend", listener);
} }
pub fn set_onreadystatechange(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, page: *Page) !void {
if (self.onreadystatechange_cbk) |cbk| try self.unregister("readystatechange", cbk.id);
self.onreadystatechange_cbk = try self.register(page.arena, "readystatechange", listener);
}
}; };

View File

@@ -138,6 +138,13 @@ pub const XMLHttpRequest = struct {
done = 4, done = 4,
}; };
// class attributes
pub const _UNSENT = @intFromEnum(State.unsent);
pub const _OPENED = @intFromEnum(State.opened);
pub const _HEADERS_RECEIVED = @intFromEnum(State.headers_received);
pub const _LOADING = @intFromEnum(State.loading);
pub const _DONE = @intFromEnum(State.done);
// https://xhr.spec.whatwg.org/#response-type // https://xhr.spec.whatwg.org/#response-type
const ResponseType = enum { const ResponseType = enum {
Empty, Empty,
@@ -360,6 +367,8 @@ pub const XMLHttpRequest = struct {
// We can we defer event destroy once the event is dispatched. // We can we defer event destroy once the event is dispatched.
defer parser.eventDestroy(evt); defer parser.eventDestroy(evt);
try parser.eventSetInternalType(evt, .xhr_event);
try parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true }); try parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt); _ = try parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt);
} }
@@ -579,11 +588,27 @@ pub const XMLHttpRequest = struct {
} }
fn onErr(self: *XMLHttpRequest, err: anyerror) void { fn onErr(self: *XMLHttpRequest, err: anyerror) void {
self.state = .done;
self.send_flag = false; self.send_flag = false;
self.dispatchEvt("readystatechange");
self.dispatchProgressEvent("error", .{}); // capture the state before we change it
self.dispatchProgressEvent("loadend", .{}); const s = self.state;
const is_abort = err == DOMError.Abort;
if (is_abort) {
self.state = .unsent;
} else {
self.state = .done;
self.dispatchEvt("error");
}
if (s != .done or s != .unsent) {
self.dispatchEvt("readystatechange");
if (is_abort) {
self.dispatchProgressEvent("abort", .{});
}
self.dispatchProgressEvent("loadend", .{});
}
const level: log.Level = if (err == DOMError.Abort) .debug else .err; const level: log.Level = if (err == DOMError.Abort) .debug else .err;
log.log(.http, level, "error", .{ log.log(.http, level, "error", .{
@@ -922,4 +947,26 @@ test "Browser.XHR.XMLHttpRequest" {
// So the url has been retrieved. // So the url has been retrieved.
.{ "status", "200" }, .{ "status", "200" },
}, .{}); }, .{});
try runner.testCases(&.{
.{ "const req6 = new XMLHttpRequest()", null },
.{
\\ var readyStates = [];
\\ var currentTarget = null;
\\ req6.onreadystatechange = (e) => {
\\ currentTarget = e.currentTarget;
\\ readyStates.push(req6.readyState);
\\ }
,
null,
},
.{ "req6.open('GET', 'https://127.0.0.1:9581/xhr')", null },
.{ "req6.send()", null },
.{ "readyStates.length", "4" },
.{ "readyStates[0] === XMLHttpRequest.OPENED", "true" },
.{ "readyStates[1] === XMLHttpRequest.HEADERS_RECEIVED", "true" },
.{ "readyStates[2] === XMLHttpRequest.LOADING", "true" },
.{ "readyStates[3] === XMLHttpRequest.DONE", "true" },
.{ "currentTarget == req6", "true" },
}, .{});
} }