Merge pull request #166 from lightpanda-io/xhr

XHR
This commit is contained in:
Pierre Tachoire
2024-02-15 16:54:06 +01:00
committed by GitHub
14 changed files with 3112 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ const Console = @import("jsruntime").Console;
const DOM = @import("dom/dom.zig");
const HTML = @import("html/html.zig");
const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -14,4 +15,5 @@ pub const Interfaces = generate.Tuple(.{
DOM.Interfaces,
Events.Interfaces,
HTML.Interfaces,
XHR.Interfaces,
});

1627
src/async/Client.zig Normal file

File diff suppressed because it is too large Load Diff

115
src/async/stream.zig Normal file
View File

@@ -0,0 +1,115 @@
const std = @import("std");
const builtin = @import("builtin");
const os = std.os;
const io = std.io;
const assert = std.debug.assert;
const tcp = @import("tcp.zig");
pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: std.os.socket_t,
pub fn close(self: Stream) void {
os.closeSocket(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = os.ReadError;
pub const WriteError = os.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
pub fn reader(self: Stream) Reader {
return .{ .context = self };
}
pub fn writer(self: Stream) Writer {
return .{ .context = self };
}
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
else => return error.Unexpected,
};
}
pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize {
return os.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
/// `buffer.len`, it means the stream reached the end. Reaching the end of
/// a stream is not an error condition.
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
return readAtLeast(s, buffer, buffer.len);
}
/// Returns the number of bytes read, calling the underlying read function
/// the minimal number of times until the buffer has at least `len` bytes
/// filled. If the number read is less than `len` it means the stream
/// reached the end. Reaching the end of the stream is not an error
/// condition.
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try s.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
/// file system thread instead of non-blocking. It needs to be reworked to properly
/// use non-blocking I/O.
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
error.AccessDenied => error.AccessDenied,
error.WouldBlock => error.WouldBlock,
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
error.MessageTooBig => error.FileTooBig,
error.BrokenPipe => error.BrokenPipe,
else => return error.Unexpected,
};
}
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
return try self.write(first_buffer);
}
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []os.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
}
}
};

94
src/async/tcp.zig Normal file
View File

@@ -0,0 +1,94 @@
const std = @import("std");
const net = std.net;
const Stream = @import("stream.zig").Stream;
const Loop = @import("jsruntime").Loop;
const NetworkImpl = Loop.Network(Conn.Command);
// Conn is a TCP connection using jsruntime Loop async I/O.
// connect, send and receive are blocking, but use async I/O in the background.
// Client doesn't own the socket used for the connection, the caller is
// responsible for closing it.
pub const Conn = struct {
const Command = struct {
impl: NetworkImpl,
done: bool = false,
err: ?anyerror = null,
ln: usize = 0,
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
self.err = err;
self.ln = ln;
self.done = true;
}
fn wait(self: *Command) !usize {
while (!self.done) try self.impl.tick();
if (self.err) |err| return err;
return self.ln;
}
pub fn onConnect(self: *Command, err: ?anyerror) void {
self.ok(err, 0);
}
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
};
loop: *Loop,
pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.os.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.os.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
}
};
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
// TODO async resolve
const list = try net.getAddressList(alloc, name, port);
defer list.deinit();
if (list.addrs.len == 0) return error.UnknownHostName;
for (list.addrs) |addr| {
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
error.ConnectionRefused => {
continue;
},
else => return err,
};
}
return std.os.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.os.socket(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP);
errdefer std.os.closeSocket(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };
try conn.connect(sockfd, addr);
return Stream{
.alloc = alloc,
.conn = conn,
.handle = sockfd,
};
}

172
src/async/test.zig Normal file
View File

@@ -0,0 +1,172 @@
const std = @import("std");
const http = std.http;
const Client = @import("Client.zig");
const Request = @import("Client.zig").Request;
pub const Loop = @import("jsruntime").Loop;
const url = "https://w3.org";
test "blocking mode fetch API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var res = try client.fetch(alloc, .{
.location = .{ .uri = try std.Uri.parse(url) },
.payload = .none,
});
defer res.deinit();
try std.testing.expect(res.status == .ok);
}
test "blocking mode open/send/wait API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{});
defer headers.deinit();
var req = try client.open(.GET, try std.Uri.parse(url), headers, .{});
defer req.deinit();
try req.send(.{});
try req.finish();
try req.wait();
try std.testing.expect(req.response.status == .ok);
}
// Example how to write an async http client using the modified standard client.
const AsyncClient = struct {
cli: Client,
const YieldImpl = Loop.Yield(AsyncRequest);
const AsyncRequest = struct {
const State = enum { new, open, send, finish, wait, done };
cli: *Client,
uri: std.Uri,
headers: std.http.Headers,
req: ?Request = undefined,
state: State = .new,
impl: YieldImpl,
err: ?anyerror = null,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
self.headers.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
self.state = .new;
return self.impl.yield(self);
}
fn onerr(self: *AsyncRequest, err: anyerror) void {
self.state = .done;
self.err = err;
}
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send(.{}) catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
self.req.?.finish() catch |e| return self.onerr(e);
},
.finish => {
self.state = .wait;
self.req.?.wait() catch |e| return self.onerr(e);
},
.wait => {
self.state = .done;
return;
},
.done => return,
}
return self.impl.yield(self);
}
pub fn wait(self: *AsyncRequest) !void {
while (self.state != .done) try self.impl.tick();
if (self.err) |err| return err;
}
};
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
return .{
.cli = .{
.allocator = alloc,
.loop = loop,
},
};
}
pub fn deinit(self: *AsyncClient) void {
self.cli.deinit();
}
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
return .{
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
.headers = .{ .allocator = self.cli.allocator, .owned = false },
};
}
};
test "non blocking client" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client = AsyncClient.init(alloc, &loop);
defer client.deinit();
var reqs: [3]AsyncClient.AsyncRequest = undefined;
for (0..reqs.len) |i| {
reqs[i] = try client.createRequest(try std.Uri.parse(url));
reqs[i].fetch();
}
for (0..reqs.len) |i| {
try reqs[i].wait();
reqs[i].deinit();
}
}

View File

@@ -176,7 +176,15 @@ pub const Page = struct {
log.info("GET {any} {d}", .{ self.uri, req.response.status });
// TODO handle redirection
if (req.response.status != .ok) return error.BadStatusCode;
if (req.response.status != .ok) {
log.debug("{?} {d} {s}\n{any}", .{
req.response.version,
req.response.status,
req.response.reason,
req.response.headers,
});
return error.BadStatusCode;
}
// TODO handle charset
// https://html.spec.whatwg.org/#content-type

View File

@@ -17,6 +17,7 @@ params: []const u8 = "",
charset: ?[]const u8 = null,
boundary: ?[]const u8 = null,
pub const Empty = Self{ .mtype = "", .msubtype = "" };
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };

View File

@@ -13,6 +13,16 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventTargetUnion = @import("../dom/event_target.zig").Union;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
// Event interfaces
pub const Interfaces = generate.Tuple(.{
Event,
ProgressEvent,
});
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
// https://dom.spec.whatwg.org/#event
pub const Event = struct {
pub const Self = parser.Event;
@@ -28,6 +38,13 @@ pub const Event = struct {
pub const _AT_TARGET = 2;
pub const _BUBBLING_PHASE = 3;
pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) {
.event => .{ .Event = evt },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
};
}
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
const event = try parser.eventCreate();
try parser.eventInit(event, eventType, opts orelse EventInit{});
@@ -104,13 +121,6 @@ pub const Event = struct {
}
};
// Event interfaces
pub const Interfaces = generate.Tuple(.{
Event,
});
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
@@ -198,4 +208,13 @@ pub fn testExecFn(
.{ .src = "nb", .ex = "1" },
};
try checkCases(js_env, &legacy);
var remove = [_]Case{
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
.{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
.{ .src = "nb", .ex = "0" },
};
try checkCases(js_env, &remove);
}

View File

@@ -4,9 +4,11 @@ const c = @cImport({
@cInclude("dom/dom.h");
@cInclude("dom/bindings/hubbub/parser.h");
@cInclude("events/event_target.h");
@cInclude("events/event.h");
});
const Callback = @import("jsruntime").Callback;
const EventToInterface = @import("events/event.zig").Event.toInterface;
// Vtable
// ------
@@ -360,6 +362,10 @@ pub const EventInit = struct {
composed: bool = false,
};
pub fn eventDestroy(evt: *Event) void {
c._dom_event_destroy(evt);
}
pub fn eventInit(evt: *Event, typ: []const u8, opts: EventInit) !void {
const s = try strFromData(typ);
const err = c._dom_event_init(evt, s, opts.bubbles, opts.cancelable);
@@ -444,6 +450,23 @@ pub fn eventPreventDefault(evt: *Event) !void {
try DOMErr(err);
}
pub fn eventGetInternalType(evt: *Event) !EventType {
var res: u32 = undefined;
const err = c._dom_event_get_internal_type(evt, &res);
try DOMErr(err);
return @enumFromInt(res);
}
pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void {
const err = c._dom_event_set_internal_type(evt, @intFromEnum(internal_type));
try DOMErr(err);
}
pub const EventType = enum(u8) {
event = 0,
progress_event = 1,
};
// EventHandler
fn event_handler_cbk(data: *anyopaque) *Callback {
const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data);
@@ -454,7 +477,14 @@ const event_handler = struct {
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
if (data) |d| {
const func = event_handler_cbk(d);
func.call(.{event}) catch unreachable;
if (event) |evt| {
func.call(.{
EventToInterface(evt) catch unreachable,
}) catch unreachable;
} else {
func.call(.{event}) catch unreachable;
}
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
@@ -671,7 +701,12 @@ pub const EventTargetTBase = extern struct {
pub fn dispatch_event(et: [*c]c.dom_event_target, evt: ?*c.struct_dom_event, res: [*c]bool) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_BUBBLING_PHASE, res);
// Set the event target to the target dispatched.
const e = c._dom_event_set_target(evt, et);
if (e != c.DOM_NO_ERR) {
return e;
}
return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_AT_TARGET, res);
}
pub fn remove_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {

View File

@@ -7,6 +7,7 @@ const generate = @import("generate.zig");
const parser = @import("netsurf.zig");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
const xhr = @import("xhr/xhr.zig");
const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -23,6 +24,8 @@ const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
const EventTestExecFn = @import("events/event.zig").testExecFn;
const XHRTestExecFn = xhr.testExecFn;
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
@@ -78,6 +81,8 @@ fn testsAllExecFn(
AttrTestExecFn,
EventTargetTestExecFn,
EventTestExecFn,
XHRTestExecFn,
ProgressEventTestExecFn,
};
inline for (testFns) |testFn| {
@@ -93,6 +98,11 @@ pub fn main() !void {
}
}
test {
const TestAsync = @import("async/test.zig");
std.testing.refAllDecls(TestAsync);
}
test "jsruntime" {
// generate tests
try generate.tests();
@@ -157,3 +167,20 @@ test "DocumentHTML is a libdom event target" {
const et = parser.toEventTarget(parser.DocumentHTML, doc);
_ = try parser.eventTargetDispatchEvent(et, event);
}
test "XMLHttpRequest.validMethod" {
// valid methods
for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| {
_ = try xhr.XMLHttpRequest.validMethod(tc);
}
// forbidden
for ([_][]const u8{ "connect", "CONNECT" }) |tc| {
try std.testing.expectError(parser.DOMError.Security, xhr.XMLHttpRequest.validMethod(tc));
}
// syntax
for ([_][]const u8{ "foo", "BAR" }) |tc| {
try std.testing.expectError(parser.DOMError.Syntax, xhr.XMLHttpRequest.validMethod(tc));
}
}

96
src/xhr/event_target.zig Normal file
View File

@@ -0,0 +1,96 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const parser = @import("../netsurf.zig");
const log = std.log.scoped(.xhr);
pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget;
pub const mem_guarantied = true;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
onloadstart_cbk: ?Callback = null,
onprogress_cbk: ?Callback = null,
onabort_cbk: ?Callback = null,
onload_cbk: ?Callback = null,
ontimeout_cbk: ?Callback = null,
onloadend_cbk: ?Callback = null,
fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false);
}
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id());
if (lst == null) {
return;
}
// remove listener
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false);
}
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
return self.onloadstart_cbk;
}
pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback {
return self.onprogress_cbk;
}
pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback {
return self.onabort_cbk;
}
pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback {
return self.onload_cbk;
}
pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback {
return self.ontimeout_cbk;
}
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback {
return self.onloadend_cbk;
}
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk);
try self.register(alloc, "loadstart", handler);
self.onloadstart_cbk = handler;
}
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk);
try self.register(alloc, "progress", handler);
self.onprogress_cbk = handler;
}
pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk);
try self.register(alloc, "abort", handler);
self.onabort_cbk = handler;
}
pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk);
try self.register(alloc, "load", handler);
self.onload_cbk = handler;
}
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk);
try self.register(alloc, "timeout", handler);
self.ontimeout_cbk = handler;
}
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk);
try self.register(alloc, "loadend", handler);
self.onloadend_cbk = handler;
}
pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| {
log.err("remove all listeners: {any}", .{e});
};
}
};

View File

@@ -0,0 +1,72 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
const DOMException = @import("../dom/exceptions.zig").DOMException;
pub const ProgressEvent = struct {
pub const prototype = *Event;
pub const Exception = DOMException;
pub const mem_guarantied = true;
pub const EventInit = struct {
lengthComputable: bool = false,
loaded: u64 = 0,
total: u64 = 0,
};
proto: parser.Event,
lengthComputable: bool,
loaded: u64 = 0,
total: u64 = 0,
pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, eventType, .{});
try parser.eventSetInternalType(event, .progress_event);
const o = opts orelse EventInit{};
return .{
.proto = event.*,
.lengthComputable = o.lengthComputable,
.loaded = o.loaded,
.total = o.total,
};
}
pub fn get_lengthComputable(self: ProgressEvent) bool {
return self.lengthComputable;
}
pub fn get_loaded(self: ProgressEvent) u64 {
return self.loaded;
}
pub fn get_total(self: ProgressEvent) u64 {
return self.total;
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var progress_event = [_]Case{
.{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" },
.{ .src = "pevt.loaded", .ex = "0" },
.{ .src = "pevt instanceof ProgressEvent", .ex = "true" },
.{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" },
.{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" },
.{ .src = "document.dispatchEvent(pevt)", .ex = "true" },
.{ .src = "eevt.type", .ex = "foo" },
.{ .src = "eevt instanceof ProgressEvent", .ex = "true" },
};
try checkCases(js_env, &progress_event);
}

833
src/xhr/xhr.zig Normal file
View File

@@ -0,0 +1,833 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("../netsurf.zig").DOMError;
const DOMException = @import("../dom/exceptions.zig").DOMException;
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
const Mime = @import("../browser/mime.zig");
const Loop = jsruntime.Loop;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
const parser = @import("../netsurf.zig");
const log = std.log.scoped(.xhr);
// XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
pub const Interfaces = generate.Tuple(.{
XMLHttpRequestEventTarget,
XMLHttpRequestUpload,
XMLHttpRequest,
});
pub const XMLHttpRequestUpload = struct {
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
};
pub const XMLHttpRequestBodyInitTag = enum {
Blob,
BufferSource,
FormData,
URLSearchParams,
String,
};
pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
Blob: []const u8,
BufferSource: []const u8,
FormData: []const u8,
URLSearchParams: []const u8,
String: []const u8,
fn contentType(self: XMLHttpRequestBodyInit) ![]const u8 {
return switch (self) {
.Blob => error.NotImplemented,
.BufferSource => error.NotImplemented,
.FormData => "multipart/form-data; boundary=TODO",
.URLSearchParams => "application/x-www-form-urlencoded; charset=UTF-8",
.String => "text/plain; charset=UTF-8",
};
}
// Duplicate the body content.
// The caller owns the allocated string.
fn dupe(self: XMLHttpRequestBodyInit, alloc: std.mem.Allocator) ![]const u8 {
return switch (self) {
.Blob => error.NotImplemented,
.BufferSource => error.NotImplemented,
.FormData => error.NotImplemented,
.URLSearchParams => error.NotImplemented,
.String => |v| try alloc.dupe(u8, v),
};
}
};
pub const XMLHttpRequest = struct {
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
pub const UNSENT: u16 = 0;
pub const OPENED: u16 = 1;
pub const HEADERS_RECEIVED: u16 = 2;
pub const LOADING: u16 = 3;
pub const DONE: u16 = 4;
// https://xhr.spec.whatwg.org/#response-type
const ResponseType = enum {
Empty,
Text,
ArrayBuffer,
Blob,
Document,
JSON,
};
// TODO use std.json.Value instead, but it causes comptime error.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/204
// const JSONValue = std.json.Value;
const JSONValue = u8;
const Response = union(ResponseType) {
Empty: void,
Text: []const u8,
ArrayBuffer: void,
Blob: void,
Document: *parser.Document,
JSON: JSONValue,
};
const ResponseObjTag = enum {
Document,
Failure,
JSON,
};
const ResponseObj = union(ResponseObjTag) {
Document: *parser.Document,
Failure: bool,
JSON: std.json.Parsed(JSONValue),
fn deinit(self: ResponseObj) void {
return switch (self) {
.Document => |d| {
const doc = @as(*parser.DocumentHTML, @ptrCast(d));
parser.documentHTMLClose(doc) catch {};
},
.JSON => |p| p.deinit(),
.Failure => {},
};
}
};
const PrivState = enum { new, open, send, write, finish, wait, done };
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: Client,
impl: YieldImpl,
priv_state: PrivState = .new,
req: ?Client.Request = null,
method: std.http.Method,
state: u16,
url: ?[]const u8,
uri: std.Uri,
headers: std.http.Headers,
sync: bool = true,
err: ?anyerror = null,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// upload: ?XMLHttpRequestUpload = null,
timeout: u32 = 0,
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null,
response_type: ResponseType = .Empty,
response_headers: std.http.Headers,
response_status: u10 = 0,
response_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
payload: ?[]const u8 = null,
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest {
return .{
.alloc = alloc,
.headers = .{ .allocator = alloc, .owned = true },
.response_headers = .{ .allocator = alloc, .owned = true },
.impl = YieldImpl.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
.state = UNSENT,
// TODO retrieve the HTTP client globally to reuse existing connections.
.cli = .{ .allocator = alloc, .loop = loop },
};
}
pub fn reset(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
if (self.url) |v| alloc.free(v);
self.url = null;
if (self.payload) |v| alloc.free(v);
self.payload = null;
if (self.response_bytes) |v| alloc.free(v);
if (self.response_obj) |v| v.deinit();
self.response_obj = null;
self.response_mime = Mime.Empty;
self.response_type = .Empty;
// TODO should we clearRetainingCapacity instead?
self.headers.clearAndFree();
self.response_headers.clearAndFree();
self.response_status = 0;
self.send_flag = false;
self.priv_state = .new;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
self.reset();
self.headers.deinit();
self.response_headers.deinit();
self.proto.deinit(alloc);
// TODO the client must be shared between requests.
self.cli.deinit();
}
pub fn get_readyState(self: *XMLHttpRequest) u16 {
return self.state;
}
pub fn get_timeout(self: *XMLHttpRequest) u32 {
return self.timeout;
}
pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void {
// TODO If the current global object is a Window object and thiss
// synchronous flag is set, then throw an "InvalidAccessError"
// DOMException.
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout
self.timeout = timeout;
}
pub fn get_withCredentials(self: *XMLHttpRequest) bool {
return self.withCredentials;
}
pub fn set_withCredentials(self: *XMLHttpRequest, withCredentials: bool) !void {
if (self.state != OPENED and self.state != UNSENT) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
self.withCredentials = withCredentials;
}
pub fn _open(
self: *XMLHttpRequest,
alloc: std.mem.Allocator,
method: []const u8,
url: []const u8,
asyn: ?bool,
username: ?[]const u8,
password: ?[]const u8,
) !void {
_ = username;
_ = password;
// TODO If thiss relevant global object is a Window object and its
// associated Document is not fully active, then throw an
// "InvalidStateError" DOMException.
self.method = try validMethod(method);
self.reset(alloc);
self.url = try alloc.dupe(u8, url);
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
self.sync = if (asyn) |b| !b else false;
self.state = OPENED;
self.dispatchEvt("readystatechange");
}
// dispatch request event.
// errors are logged only.
fn dispatchEvt(self: *XMLHttpRequest, typ: []const u8) void {
const evt = parser.eventCreate() catch |e| {
return log.err("dispatch event create: {any}", .{e});
};
// We can we defer event destroy once the event is dispatched.
defer parser.eventDestroy(evt);
parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true }) catch |e| {
return log.err("dispatch event init: {any}", .{e});
};
_ = parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt) catch |e| {
return log.err("dispatch event: {any}", .{e});
};
}
fn dispatchProgressEvent(
self: *XMLHttpRequest,
typ: []const u8,
opts: ProgressEvent.EventInit,
) void {
var evt = ProgressEvent.constructor(typ, .{
// https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface
.lengthComputable = opts.total > 0,
.total = opts.total,
.loaded = opts.loaded,
}) catch |e| {
return log.err("construct progress event: {any}", .{e});
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(self)),
@as(*parser.Event, @ptrCast(&evt)),
) catch |e| {
return log.err("dispatch progress event: {any}", .{e});
};
}
const methods = [_]struct {
tag: std.http.Method,
name: []const u8,
}{
.{ .tag = .DELETE, .name = "DELETE" },
.{ .tag = .GET, .name = "GET" },
.{ .tag = .HEAD, .name = "HEAD" },
.{ .tag = .OPTIONS, .name = "OPTIONS" },
.{ .tag = .POST, .name = "POST" },
.{ .tag = .PUT, .name = "PUT" },
};
const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" };
pub fn validMethod(m: []const u8) DOMError!std.http.Method {
for (methods) |method| {
if (std.ascii.eqlIgnoreCase(method.name, m)) {
return method.tag;
}
}
// If method is a forbidden method, then throw a "SecurityError" DOMException.
for (methods_forbidden) |method| {
if (std.ascii.eqlIgnoreCase(method, m)) {
return DOMError.Security;
}
}
// If method is not a method, then throw a "SyntaxError" DOMException.
return DOMError.Syntax;
}
pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
return try self.headers.append(name, value);
}
// TODO body can be either a XMLHttpRequestBodyInit or a document
pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
// The body argument provides the request body, if any, and is ignored
// if the request method is GET or HEAD.
// https://xhr.spec.whatwg.org/#the-send()-method
// var used_body: ?XMLHttpRequestBodyInit = null;
if (body != null and self.method != .GET and self.method != .HEAD) {
// TODO If body is a Document, then set thiss request body to body, serialized, converted, and UTF-8 encoded.
const body_init = XMLHttpRequestBodyInit{ .String = body.? };
// keep the user content type from request headers.
if (self.headers.getFirstEntry("Content-Type") == null) {
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
try self.headers.append("Content-Type", try body_init.contentType());
}
// copy the payload
if (self.payload) |v| alloc.free(v);
self.payload = try body_init.dupe(alloc);
}
log.debug("{any} {any}", .{ self.method, self.uri });
self.send_flag = true;
self.impl.yield(self);
}
// onYield is a callback called between each request's steps.
// Between each step, the code is blocking.
// Yielding allows pseudo-async and gives a chance to other async process
// to be called.
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
if (err) |e| return self.onErr(e);
switch (self.priv_state) {
.new => {
self.priv_state = .open;
self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e);
},
.open => {
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
self.req.?.send(.{}) catch |e| return self.onErr(e);
},
.send => {
if (self.payload) |payload| {
self.priv_state = .write;
self.req.?.writeAll(payload) catch |e| return self.onErr(e);
} else {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
}
},
.write => {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
},
.finish => {
self.priv_state = .wait;
self.req.?.wait() catch |e| return self.onErr(e);
},
.wait => {
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// TODO dispatch only if 50ms have passed.
self.state = LOADING;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
},
.done => {
if (self.req) |*r| {
r.deinit();
self.req = null;
}
// finalize fetch process.
return;
},
}
self.impl.yield(self);
}
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
self.priv_state = .done;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
self.err = err;
self.state = DONE;
self.send_flag = false;
self.dispatchEvt("readystatechange");
self.dispatchProgressEvent("error", .{});
self.dispatchProgressEvent("loadend", .{});
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
}
pub fn _abort(self: *XMLHttpRequest) void {
self.onErr(DOMError.Abort);
}
pub fn get_responseType(self: *XMLHttpRequest) []const u8 {
return switch (self.response_type) {
.Empty => "",
.ArrayBuffer => "arraybuffer",
.Blob => "blob",
.Document => "document",
.JSON => "json",
.Text => "text",
};
}
pub fn set_responseType(self: *XMLHttpRequest, rtype: []const u8) !void {
if (self.state == LOADING or self.state == DONE) return DOMError.InvalidState;
if (std.mem.eql(u8, rtype, "")) {
self.response_type = .Empty;
return;
}
if (std.mem.eql(u8, rtype, "arraybuffer")) {
self.response_type = .ArrayBuffer;
return;
}
if (std.mem.eql(u8, rtype, "blob")) {
self.response_type = .Blob;
return;
}
if (std.mem.eql(u8, rtype, "document")) {
self.response_type = .Document;
return;
}
if (std.mem.eql(u8, rtype, "json")) {
self.response_type = .JSON;
return;
}
if (std.mem.eql(u8, rtype, "text")) {
self.response_type = .Text;
return;
}
}
// TODO retrieve the redirected url
pub fn get_responseURL(self: *XMLHttpRequest) ?[]const u8 {
return self.url;
}
pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
if (self.response_type != .Empty and self.response_type != .Document) {
return DOMError.InvalidState;
}
if (self.state != DONE) return null;
// fastpath if response is previously parsed.
if (self.response_obj) |obj| {
return switch (obj) {
.Failure => null,
.Document => |v| .{ .Document = v },
.JSON => null,
};
}
self.setResponseObjDocument(alloc);
if (self.response_obj) |obj| {
return switch (obj) {
.Failure => null,
.Document => |v| .{ .Document = v },
.JSON => null,
};
}
return null;
}
// https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
return .{ .Text = try self.get_responseText() };
}
// fastpath if response is previously parsed.
if (self.response_obj) |obj| {
return switch (obj) {
.Failure => null,
.Document => |v| .{ .Document = v },
.JSON => |v| .{ .JSON = v.value },
};
}
if (self.response_type == .ArrayBuffer) {
// TODO If thiss response type is "arraybuffer", then set thiss
// response object to a new ArrayBuffer object representing thiss
// received bytes. If this throws an exception, then set thiss
// response object to failure and return null.
return null;
}
if (self.response_type == .Blob) {
// TODO Otherwise, if thiss response type is "blob", set thiss
// response object to a new Blob object representing thiss
// received bytes with type set to the result of get a final MIME
// type for this.
return null;
}
// Otherwise, if thiss response type is "document", set a
// document response for this.
if (self.response_type == .Document) {
self.setResponseObjDocument(alloc);
}
if (self.response_type == .JSON) {
if (self.response_bytes == null) return null;
// TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return
// null.
self.setResponseObjJSON(alloc);
}
if (self.response_obj) |obj| {
return switch (obj) {
.Failure => null,
.Document => |v| .{ .Document = v },
.JSON => |v| .{ .JSON = v.value },
};
}
return null;
}
// setResponseObjDocument parses the received bytes as HTML document and
// stores the result into response_obj.
// If the par sing fails, a Failure is stored in response_obj.
// TODO parse XML.
// https://xhr.spec.whatwg.org/#response-object
fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
const isHTML = self.response_mime.eql(Mime.HTML);
// TODO If finalMIME is not an HTML MIME type or an XML MIME type, then
// return.
if (!isHTML) return;
const ccharset = alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch {
self.response_obj = .{ .Failure = true };
return;
};
defer alloc.free(ccharset);
var fbs = std.io.fixedBufferStream(self.response_bytes.?);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
self.response_obj = .{ .Failure = true };
return;
};
// TODO Set documents URL to xhrs responses URL.
// TODO Set documents origin to xhrs relevant settings objects origin.
self.response_obj = .{
.Document = parser.documentHTMLToDocument(doc),
};
}
// setResponseObjJSON parses the received bytes as a std.json.Value.
fn setResponseObjJSON(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is
// already an arena?
const p = std.json.parseFromSlice(
JSONValue,
alloc,
self.response_bytes.?,
.{},
) catch |e| {
log.err("parse JSON: {}", .{e});
self.response_obj = .{ .Failure = true };
return;
};
self.response_obj = .{ .JSON = p };
}
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
return if (self.response_bytes) |v| v else "";
}
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {
return self.response_headers.getFirstValue(name);
}
// The caller owns the string returned.
// TODO change the return type to express the string ownership and let
// jsruntime free the string once copied to v8.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 {
if (self.response_headers.list.items.len == 0) return "";
self.response_headers.sort();
var buf: std.ArrayListUnmanaged(u8) = .{};
errdefer buf.deinit(alloc);
const w = buf.writer(alloc);
for (self.response_headers.list.items) |entry| {
if (entry.value.len == 0) continue;
try w.writeAll(entry.name);
try w.writeAll(": ");
try w.writeAll(entry.value);
try w.writeAll("\r\n");
}
return buf.items;
}
pub fn get_status(self: *XMLHttpRequest) u16 {
return self.response_status;
}
pub fn get_statusText(self: *XMLHttpRequest) []const u8 {
if (self.response_status == 0) return "";
return std.http.Status.phrase(@enumFromInt(self.response_status)) orelse "";
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var send = [_]Case{
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", .ex = "undefined" },
.{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
// Getter returning a callback crashes.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
// ensure open resets values
.{ .src = "req.status", .ex = "0" },
.{ .src = "req.statusText", .ex = "" },
.{ .src = "req.getAllResponseHeaders()", .ex = "" },
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" },
.{ .src = "req.responseText", .ex = "" },
.{ .src = "req.send(); nb", .ex = "0" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "nb", .ex = "1" },
.{ .src = "evt.type", .ex = "load" },
.{ .src = "evt.loaded > 0", .ex = "true" },
.{ .src = "evt instanceof ProgressEvent", .ex = "true" },
.{ .src = "req.status", .ex = "200" },
.{ .src = "req.statusText", .ex = "OK" },
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
.{ .src = "req.responseText.length > 64", .ex = "true" },
.{ .src = "req.response", .ex = "" },
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
};
try checkCases(js_env, &send);
var document = [_]Case{
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.responseType = 'document'", .ex = "document" },
.{ .src = "req2.send()", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "req2.status", .ex = "200" },
.{ .src = "req2.statusText", .ex = "OK" },
.{ .src = "req2.response instanceof Document", .ex = "true" },
.{ .src = "req2.responseXML instanceof Document", .ex = "true" },
};
try checkCases(js_env, &document);
// var json = [_]Case{
// .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
// .{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
// .{ .src = "req3.responseType = 'json'", .ex = "json" },
// .{ .src = "req3.send()", .ex = "undefined" },
// // Each case executed waits for all loop callaback calls.
// // So the url has been retrieved.
// .{ .src = "req3.status", .ex = "200" },
// .{ .src = "req3.statusText", .ex = "OK" },
// .{ .src = "req3.response", .ex = "" },
// };
// try checkCases(js_env, &json);
//
var post = [_]Case{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req3.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req3.send('foo')", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "req3.status", .ex = "200" },
.{ .src = "req3.statusText", .ex = "OK" },
.{ .src = "req3.responseText.length > 64", .ex = "true" },
};
try checkCases(js_env, &post);
}