Add readystate change event to XHR

Deal with non-node current target crashing. Builds ontop of abort_signal, but
with the new event-target specific union, I think this will work in for all
future cases.
This commit is contained in:
Karl Seguin
2025-07-03 19:27:16 +08:00
parent bf1db50667
commit dfe90243d6
6 changed files with 55 additions and 9 deletions

View File

@@ -23,10 +23,12 @@ const Page = @import("../page.zig").Page;
const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig");
const nod = @import("node.zig");
// EventTarget interfaces
pub const Union = Nod.Union;
pub const Union = union(enum) {
node: nod.Union,
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
};
// EventTarget implementation
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
// its a singleton.
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
// so we hijack the event internal type to identity if.
switch (try parser.eventGetInternalType(e)) {
.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 => {
// some of these probably need to be special-cased like abort_signal
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
return .{ .node = try 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 {
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)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },

View File

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

View File

@@ -39,6 +39,7 @@ pub const XMLHttpRequestEventTarget = struct {
onload_cbk: ?Function = null,
ontimeout_cbk: ?Function = null,
onloadend_cbk: ?Function = null,
onreadystatechange_cbk: ?Function = null,
fn register(
self: *XMLHttpRequestEventTarget,
@@ -86,6 +87,9 @@ pub const XMLHttpRequestEventTarget = struct {
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function {
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 {
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);
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,
};
// 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
const ResponseType = enum {
Empty,
@@ -360,6 +367,8 @@ pub const XMLHttpRequest = struct {
// We can we defer event destroy once the event is dispatched.
defer parser.eventDestroy(evt);
try parser.eventSetInternalType(evt, .xhr_event);
try parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt);
}
@@ -923,4 +932,26 @@ test "Browser.XHR.XMLHttpRequest" {
// So the url has been retrieved.
.{ "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" },
}, .{});
}