diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 505eb6e8..5bc1f7fc 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -201,7 +201,11 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
else
"no value";
- log.debug(.js, "unhandled rejection", .{ .value = value, .stack = context.stackTrace() catch |err| @errorName(err) orelse "???" });
+ log.debug(.js, "unhandled rejection", .{
+ .value = value,
+ .stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
+ .note = "This should be updated to call window.unhandledrejection",
+ });
}
// Give it a Zig struct, get back a v8.FunctionTemplate.
diff --git a/src/browser/tests/legacy/xhr/xhr.html b/src/browser/tests/legacy/xhr/xhr.html
index 13ab6216..2ff428b7 100644
--- a/src/browser/tests/legacy/xhr/xhr.html
+++ b/src/browser/tests/legacy/xhr/xhr.html
@@ -11,13 +11,12 @@
testing.expectEqual(cbk, req.onload);
req.onload = cbk;
- req.open('GET', 'http://127.0.0.1:9582/xhr');
+ req.open('GET', 'http://127.0.0.1:9589/xhr');
testing.expectEqual(0, req.status);
testing.expectEqual('', req.statusText);
testing.expectEqual('', req.getAllResponseHeaders());
testing.expectEqual(null, req.getResponseHeader('Content-Type'));
testing.expectEqual('', req.responseText);
-
req.send();
});
@@ -31,7 +30,6 @@
testing.expectEqual('content-length: 100\r\nContent-Type: text/html; charset=utf-8\r\n', req.getAllResponseHeaders());
testing.expectEqual(100, req.responseText.length);
testing.expectEqual(req.responseText.length, req.response.length);
- testing.expectEqual(true, req.responseXML instanceof Document);
});
@@ -39,7 +37,7 @@
const req2 = new XMLHttpRequest()
const promise2 = new Promise((resolve) => {
req2.onload = resolve;
- req2.open('GET', 'http://127.0.0.1:9582/xhr')
+ req2.open('GET', 'http://127.0.0.1:9589/xhr')
req2.responseType = 'document';
req2.send()
});
@@ -56,7 +54,7 @@
const req3 = new XMLHttpRequest()
const promise3 = new Promise((resolve) => {
req3.onload = resolve;
- req3.open('GET', 'http://127.0.0.1:9582/xhr/json')
+ req3.open('GET', 'http://127.0.0.1:9589/xhr/json')
req3.responseType = 'json';
req3.send()
});
@@ -72,7 +70,7 @@
const req4 = new XMLHttpRequest()
const promise4 = new Promise((resolve) => {
req4.onload = resolve;
- req4.open('POST', 'http://127.0.0.1:9582/xhr')
+ req4.open('POST', 'http://127.0.0.1:9589/xhr')
req4.send('foo')
});
@@ -94,7 +92,7 @@
}
}
- req5.open('GET', 'http://127.0.0.1:9582/xhr');
+ req5.open('GET', 'http://127.0.0.1:9589/xhr');
req5.send();
});
diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html
index bb73aa73..82e9b6d1 100644
--- a/src/browser/tests/net/xhr.html
+++ b/src/browser/tests/net/xhr.html
@@ -7,4 +7,195 @@
testing.expectEqual(2, XMLHttpRequest.HEADERS_RECEIVED);
testing.expectEqual(3, XMLHttpRequest.LOADING);
testing.expectEqual(4, XMLHttpRequest.DONE);
+
+ testing.async(async (restore) => {
+ const req = new XMLHttpRequest();
+ const event = await new Promise((resolve) => {
+ function cbk(event) {
+ resolve(event)
+ }
+
+ req.onload = cbk;
+ testing.expectEqual(cbk, req.onload);
+ req.onload = cbk;
+
+ req.open('GET', 'http://127.0.0.1:9582/xhr');
+ testing.expectEqual(0, req.status);
+ testing.expectEqual('', req.statusText);
+ testing.expectEqual('', req.getAllResponseHeaders());
+ testing.expectEqual(null, req.getResponseHeader('Content-Type'));
+ testing.expectEqual('', req.responseText);
+ testing.expectEqual('', req.responseURL);
+ req.send();
+ });
+
+ restore();
+ testing.expectEqual('load', event.type);
+ testing.expectEqual(true, event.loaded > 0);
+ testing.expectEqual(true, event instanceof ProgressEvent);
+ testing.expectEqual(200, req.status);
+ testing.expectEqual('OK', req.statusText);
+ testing.expectEqual('text/html; charset=utf-8', req.getResponseHeader('Content-Type'));
+ testing.expectEqual('content-length: 100\r\nContent-Type: text/html; charset=utf-8\r\n', req.getAllResponseHeaders());
+ testing.expectEqual(100, req.responseText.length);
+ testing.expectEqual(req.responseText.length, req.response.length);
+ testing.expectEqual('http://127.0.0.1:9582/xhr', req.responseURL);
+ });
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js
index 5cb6220a..afc1fa69 100644
--- a/src/browser/tests/testing.js
+++ b/src/browser/tests/testing.js
@@ -64,10 +64,8 @@
}
async function async(cb) {
- const script_id = document.currentScript.id;
- const stack = new Error().stack;
- async_capture = {script_id: script_id, stack: stack};
- await cb();
+ let capture = {script_id: document.currentScript.id, stack: new Error().stack};
+ await cb(() => { async_capture = capture; });
async_capture = null;
}
diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig
index 4959d563..f8181e10 100644
--- a/src/browser/webapi/net/XMLHttpRequest.zig
+++ b/src/browser/webapi/net/XMLHttpRequest.zig
@@ -25,6 +25,7 @@ const Http = @import("../../../http/Http.zig");
const URL = @import("../../URL.zig");
const Mime = @import("../../Mime.zig");
const Page = @import("../../Page.zig");
+const Node = @import("../Node.zig");
const Event = @import("../Event.zig");
const Headers = @import("Headers.zig");
const EventTarget = @import("../EventTarget.zig");
@@ -44,17 +45,19 @@ _method: Http.Method = .GET,
_request_headers: *Headers,
_request_body: ?[]const u8 = null,
-_response: std.ArrayList(u8) = .empty,
+_response: ?Response = null,
+_response_data: std.ArrayList(u8) = .empty,
_response_status: u16 = 0,
_response_len: ?usize = 0,
+_response_url: [:0]const u8 = "",
_response_mime: ?Mime = null,
_response_headers: std.ArrayList([]const u8) = .empty,
_response_type: ResponseType = .text,
-_state: State = .unsent,
+_ready_state: ReadyState = .unsent,
_on_ready_state_change: ?js.Function = null,
-const State = enum(u8) {
+const ReadyState = enum(u8) {
unsent = 0,
opened = 1,
headers_received = 2,
@@ -62,9 +65,16 @@ const State = enum(u8) {
done = 4,
};
+const Response = union(ResponseType) {
+ text: []const u8,
+ json: std.json.Value,
+ document: *Node.Document,
+};
+
const ResponseType = enum {
text,
json,
+ document,
// TODO: other types to support
};
@@ -100,30 +110,6 @@ pub fn setOnReadyStateChange(self: *XMLHttpRequest, cb_: ?js.Function) !void {
}
}
-pub fn getResponseType(self: *const XMLHttpRequest) []const u8 {
- return @tagName(self._response_type);
-}
-
-pub fn setResponseType(self: *XMLHttpRequest, value: []const u8) void {
- if (std.meta.stringToEnum(ResponseType, value)) |rt| {
- self._response_type = rt;
- }
-}
-
-pub fn getStatus(self: *const XMLHttpRequest) u16 {
- return self._response_status;
-}
-
-pub fn getResponse(self: *const XMLHttpRequest, page: *Page) !Response {
- switch (self._response_type) {
- .text => return .{ .text = self._response.items },
- .json => {
- const parsed = try std.json.parseFromSliceLeaky(std.json.Value, page.call_arena, self._response.items, .{});
- return .{ .json = parsed };
- },
- }
-}
-
// TODO: this takes an opitonal 3 more parameters
// TODO: url should be a union, as it can be multiple things
pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void {
@@ -168,6 +154,105 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.error_callback = httpErrorCallback,
});
}
+pub fn getReadyState(self: *const XMLHttpRequest) u32 {
+ return @intFromEnum(self._ready_state);
+}
+
+pub fn getResponseHeader(self: *const XMLHttpRequest, name: []const u8) ?[]const u8 {
+ for (self._response_headers.items) |entry| {
+ if (entry.len <= name.len) {
+ continue;
+ }
+ if (std.ascii.eqlIgnoreCase(name, entry[0..name.len]) == false) {
+ continue;
+ }
+ if (entry[name.len] != ':') {
+ continue;
+ }
+ return std.mem.trimLeft(u8, entry[name.len + 1 ..], " ");
+ }
+ return null;
+}
+
+pub fn getAllResponseHeaders(self: *const XMLHttpRequest, page: *Page) ![]const u8 {
+ if (self._ready_state != .done) {
+ // MDN says this should return null, but it seems to return an empty string
+ // in every browser. Specs are too hard for a dumbo like me to understand.
+ return "";
+ }
+
+ var buf = std.Io.Writer.Allocating.init(page.call_arena);
+ for (self._response_headers.items) |entry| {
+ try buf.writer.writeAll(entry);
+ try buf.writer.writeAll("\r\n");
+ }
+ return buf.written();
+}
+
+pub fn getResponseType(self: *const XMLHttpRequest) []const u8 {
+ if (self._ready_state != .done) {
+ return "";
+ }
+ return @tagName(self._response_type);
+}
+
+pub fn setResponseType(self: *XMLHttpRequest, value: []const u8) void {
+ if (std.meta.stringToEnum(ResponseType, value)) |rt| {
+ self._response_type = rt;
+ }
+}
+
+pub fn getResponseText(self: *const XMLHttpRequest) []const u8 {
+ return self._response_data.items;
+}
+
+pub fn getStatus(self: *const XMLHttpRequest) u16 {
+ return self._response_status;
+}
+
+pub fn getStatusText(self: *const XMLHttpRequest) []const u8 {
+ return std.http.Status.phrase(@enumFromInt(self._response_status)) orelse "";
+}
+
+pub fn getResponseURL(self: *XMLHttpRequest) []const u8 {
+ return self._response_url;
+}
+
+pub fn getResponse(self: *XMLHttpRequest, page: *Page) !?Response {
+ if (self._ready_state != .done) {
+ return null;
+ }
+
+ if (self._response) |res| {
+ // was already loaded
+ return res;
+ }
+
+ const data = self._response_data.items;
+ const res: Response = switch (self._response_type) {
+ .text => .{ .text = data },
+ .json => blk: {
+ const parsed = try std.json.parseFromSliceLeaky(std.json.Value, page.call_arena, data, .{});
+ break :blk .{ .json = parsed };
+ },
+ .document => blk: {
+ const document = try page._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });
+ try page.parseHtmlAsChildren(document.asNode(), data);
+ break :blk .{ .document = document };
+ },
+ };
+
+ self._response = res;
+ return res;
+}
+
+pub fn getResponseXML(self: *XMLHttpRequest, page: *Page) !?*Node.Document {
+ const res = (try self.getResponse(page)) orelse return null;
+ return switch (res) {
+ .document => |doc| doc,
+ else => null,
+ };
+}
fn httpStartCallback(transfer: *Http.Transfer) !void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));
@@ -211,8 +296,9 @@ fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void {
self._response_status = header.status;
if (transfer.getContentLength()) |cl| {
self._response_len = cl;
- try self._response.ensureTotalCapacity(self._arena, cl);
+ try self._response_data.ensureTotalCapacity(self._arena, cl);
}
+ self._response_url = try self._arena.dupeZ(u8, std.mem.span(header.url));
try self.stateChanged(.headers_received, self._page);
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, self._page);
@@ -221,11 +307,11 @@ fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void {
fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));
- try self._response.appendSlice(self._arena, data);
+ try self._response_data.appendSlice(self._arena, data);
try self._proto.dispatch(.progress, .{
.total = self._response_len orelse 0,
- .loaded = self._response.items.len,
+ .loaded = self._response_data.items.len,
}, self._page);
}
@@ -236,7 +322,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.source = "xhr",
.url = self._url,
.status = self._response_status,
- .len = self._response.items.len,
+ .len = self._response_data.items.len,
});
// Not that the request is done, the http/client will free the transfer
@@ -244,7 +330,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
self._transfer = null;
try self.stateChanged(.done, self._page);
- const loaded = self._response.items.len;
+ const loaded = self._response_data.items.len;
try self._proto.dispatch(.load, .{
.total = loaded,
.loaded = loaded,
@@ -262,7 +348,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
self.handleError(err);
}
-pub fn _abort(self: *XMLHttpRequest) void {
+pub fn abort(self: *XMLHttpRequest) void {
self.handleError(error.Abort);
if (self._transfer) |transfer| {
transfer.abort();
@@ -281,13 +367,14 @@ fn handleError(self: *XMLHttpRequest, err: anyerror) void {
fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
const is_abort = err == error.Abort;
- const new_state: State = if (is_abort) .unsent else .done;
- if (new_state != self._state) {
+ const new_state: ReadyState = if (is_abort) .unsent else .done;
+ if (new_state != self._ready_state) {
const page = self._page;
try self.stateChanged(new_state, page);
if (is_abort) {
try self._proto.dispatch(.abort, null, page);
}
+ try self._proto.dispatch(.err, null, page);
try self._proto.dispatch(.load_end, null, page);
}
@@ -299,9 +386,10 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
});
}
-fn stateChanged(self: *XMLHttpRequest, state: State, page: *Page) !void {
+fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
// there are more rules than this, but it's a start
- std.debug.assert(state != self._state);
+ std.debug.assert(state != self._ready_state);
+ self._ready_state = state;
const event = try Event.init("readystatechange", .{}, page);
try page._event_manager.dispatchWithFunction(
@@ -328,11 +416,6 @@ fn parseMethod(method: []const u8) !Http.Method {
return error.InvalidMethod;
}
-const Response = union(enum) {
- text: []const u8,
- json: std.json.Value,
-};
-
pub const JsApi = struct {
pub const bridge = js.Bridge(XMLHttpRequest);
@@ -343,19 +426,27 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});
- pub const UNSENT = bridge.property(@intFromEnum(XMLHttpRequest.State.unsent));
- pub const OPENED = bridge.property(@intFromEnum(XMLHttpRequest.State.opened));
- pub const HEADERS_RECEIVED = bridge.property(@intFromEnum(XMLHttpRequest.State.headers_received));
- pub const LOADING = bridge.property(@intFromEnum(XMLHttpRequest.State.loading));
- pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.State.done));
+ pub const UNSENT = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.unsent));
+ pub const OPENED = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.opened));
+ pub const HEADERS_RECEIVED = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.headers_received));
+ pub const LOADING = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.loading));
+ pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done));
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
pub const open = bridge.function(XMLHttpRequest.open, .{});
pub const send = bridge.function(XMLHttpRequest.send, .{});
pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{});
pub const status = bridge.accessor(XMLHttpRequest.getStatus, null, .{});
+ pub const statusText = bridge.accessor(XMLHttpRequest.getStatusText, null, .{});
+ pub const readyState = bridge.accessor(XMLHttpRequest.getReadyState, null, .{});
pub const response = bridge.accessor(XMLHttpRequest.getResponse, null, .{});
+ pub const responseText = bridge.accessor(XMLHttpRequest.getResponseText, null, .{});
+ pub const responseXML = bridge.accessor(XMLHttpRequest.getResponseXML, null, .{});
+ pub const responseURL = bridge.accessor(XMLHttpRequest.getResponseURL, null, .{});
pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{});
+ pub const getResponseHeader = bridge.function(XMLHttpRequest.getResponseHeader, .{});
+ pub const getAllResponseHeaders = bridge.function(XMLHttpRequest.getAllResponseHeaders, .{});
+ pub const abort = bridge.function(XMLHttpRequest.abort, .{});
};
const testing = @import("../../../testing.zig");
diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig
index 5b1cf862..0690d898 100644
--- a/src/main_legacy_test.zig
+++ b/src/main_legacy_test.zig
@@ -170,6 +170,22 @@ const TestHTTPServer = struct {
fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void {
const path = req.head.target;
+ if (std.mem.eql(u8, path, "/xhr")) {
+ return req.respond("1234567890" ** 10, .{
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "text/html; charset=utf-8" },
+ },
+ });
+ }
+
+ if (std.mem.eql(u8, path, "/xhr/json")) {
+ return req.respond("{\"over\":\"9000!!!\"}", .{
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "application/json" },
+ },
+ });
+ }
+
// strip out leading '/' to make the path relative
const file = try server.dir.openFile(path[1..], .{});
defer file.close();
diff --git a/src/testing.zig b/src/testing.zig
index f3c1aeac..256d9728 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -407,7 +407,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
test_session.fetchWait(2000);
page._session.browser.runMicrotasks();
- page._session.browser.runMessageLoop();
js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown";
@@ -508,12 +507,6 @@ fn serveCDP(wg: *std.Thread.WaitGroup) !void {
fn testHTTPHandler(req: *std.http.Server.Request) !void {
const path = req.head.target;
- if (std.mem.eql(u8, path, "/loader")) {
- return req.respond("Hello!", .{
- .extra_headers = &.{.{ .name = "Connection", .value = "close" }},
- });
- }
-
if (std.mem.eql(u8, path, "/xhr")) {
return req.respond("1234567890" ** 10, .{
.extra_headers = &.{
@@ -530,6 +523,33 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
+ if (std.mem.eql(u8, path, "/xhr/redirect")) {
+ return req.respond("", .{
+ .status = .found,
+ .extra_headers = &.{
+ .{ .name = "Location", .value = "http://127.0.0.1:9582/xhr" },
+ },
+ });
+ }
+
+ if (std.mem.eql(u8, path, "/xhr/404")) {
+ return req.respond("Not Found", .{
+ .status = .not_found,
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "text/plain" },
+ },
+ });
+ }
+
+ if (std.mem.eql(u8, path, "/xhr/500")) {
+ return req.respond("Internal Server Error", .{
+ .status = .internal_server_error,
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "text/plain" },
+ },
+ });
+ }
+
if (std.mem.startsWith(u8, path, "/src/browser/tests/")) {
// strip off leading / so that it's relative to CWD
return TestHTTPServer.sendFile(req, path[1..]);