migrate fetch tests to htmlRunner

This commit is contained in:
Muki Kiboigo
2025-09-09 13:09:03 -07:00
parent 7acf67d668
commit 0423a178e9
7 changed files with 219 additions and 115 deletions

View File

@@ -17,9 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
@@ -108,7 +111,8 @@ pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
const gop = try self.headers.getOrPut(allocator, name);
const key = try allocator.dupe(u8, name);
const gop = try self.headers.getOrPut(allocator, key);
if (gop.found_existing) {
// If we found it, append the value.
@@ -129,13 +133,13 @@ pub fn _delete(self: *Headers, name: []const u8) void {
_ = self.headers.remove(name);
}
pub const HeaderEntryIterator = struct {
pub const HeadersEntryIterator = struct {
slot: [2][]const u8,
iter: HeaderHashMap.Iterator,
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
// important that is.
pub fn _next(self: *HeaderEntryIterator) ?[2][]const u8 {
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
if (self.iter.next()) |entry| {
self.slot[0] = entry.key_ptr.*;
self.slot[1] = entry.value_ptr.*;
@@ -146,10 +150,12 @@ pub const HeaderEntryIterator = struct {
}
};
pub fn _entries(self: *const Headers) HeaderEntryIterator {
pub fn _entries(self: *const Headers) HeadersEntryIterable {
return .{
.inner = .{
.slot = undefined,
.iter = self.headers.iterator(),
},
};
}
@@ -171,10 +177,10 @@ pub fn _has(self: *const Headers, name: []const u8) bool {
return self.headers.contains(name);
}
pub const HeaderKeyIterator = struct {
pub const HeadersKeyIterator = struct {
iter: HeaderHashMap.KeyIterator,
pub fn _next(self: *HeaderKeyIterator) ?[]const u8 {
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
if (self.iter.next()) |key| {
return key.*;
} else {
@@ -183,21 +189,22 @@ pub const HeaderKeyIterator = struct {
}
};
pub fn _keys(self: *const Headers) HeaderKeyIterator {
return .{ .iter = self.headers.keyIterator() };
pub fn _keys(self: *const Headers) HeadersKeyIterable {
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
}
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
const gop = try self.headers.getOrPut(arena, name);
const key = try arena.dupe(u8, name);
const gop = try self.headers.getOrPut(arena, key);
gop.value_ptr.* = try arena.dupe(u8, value);
}
pub const HeaderValueIterator = struct {
pub const HeadersValueIterator = struct {
iter: HeaderHashMap.ValueIterator,
pub fn _next(self: *HeaderValueIterator) ?[]const u8 {
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
if (self.iter.next()) |value| {
return value.*;
} else {
@@ -206,53 +213,15 @@ pub const HeaderValueIterator = struct {
}
};
pub fn _values(self: *const Headers) HeaderValueIterator {
return .{ .iter = self.headers.valueIterator() };
pub fn _values(self: *const Headers) HeadersValueIterable {
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
}
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
const testing = @import("../../testing.zig");
test "fetch: headers" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let emptyHeaders = new Headers()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let headers = new Headers({'Set-Cookie': 'name=world'})", "undefined" },
.{ "headers.get('set-cookie')", "name=world" },
}, .{});
// adapted from the mdn examples
try runner.testCases(&.{
.{ "const myHeaders = new Headers();", "undefined" },
.{ "myHeaders.append('Content-Type', 'image/jpeg')", "undefined" },
.{ "myHeaders.has('Picture-Type')", "false" },
.{ "myHeaders.get('Content-Type')", "image/jpeg" },
.{ "myHeaders.append('Content-Type', 'image/png')", "undefined" },
.{ "myHeaders.get('Content-Type')", "image/jpeg, image/png" },
.{ "myHeaders.delete('Content-Type')", "undefined" },
.{ "myHeaders.get('Content-Type')", "null" },
.{ "myHeaders.set('Picture-Type', 'image/svg')", "undefined" },
.{ "myHeaders.get('Picture-Type')", "image/svg" },
.{ "myHeaders.has('Picture-Type')", "true" },
}, .{});
try runner.testCases(&.{
.{ "const originalHeaders = new Headers([['Content-Type', 'application/json'], ['Authorization', 'Bearer token123']])", "undefined" },
.{ "originalHeaders.get('Content-Type')", "application/json" },
.{ "originalHeaders.get('Authorization')", "Bearer token123" },
.{ "const newHeaders = new Headers(originalHeaders)", "undefined" },
.{ "newHeaders.get('Content-Type')", "application/json" },
.{ "newHeaders.get('Authorization')", "Bearer token123" },
.{ "newHeaders.has('Content-Type')", "true" },
.{ "newHeaders.has('Authorization')", "true" },
.{ "newHeaders.has('X-Custom')", "false" },
// Verify that modifying the new headers doesn't affect the original
.{ "newHeaders.set('X-Custom', 'test-value')", "undefined" },
.{ "newHeaders.get('X-Custom')", "test-value" },
.{ "originalHeaders.get('X-Custom')", "null" },
.{ "originalHeaders.has('X-Custom')", "false" },
}, .{});
test "fetch: Headers" {
try testing.htmlRunner("fetch/headers.html");
}

View File

@@ -54,6 +54,10 @@ pub const RequestCache = enum {
return null;
}
}
pub fn toString(self: RequestCache) []const u8 {
return @tagName(self);
}
};
pub const RequestCredentials = enum {
@@ -70,6 +74,10 @@ pub const RequestCredentials = enum {
return null;
}
}
pub fn toString(self: RequestCredentials) []const u8 {
return @tagName(self);
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
@@ -154,6 +162,10 @@ pub fn get_cache(self: *const Request) RequestCache {
return self.cache;
}
pub fn get_credentials(self: *const Request) RequestCredentials {
return self.credentials;
}
pub fn get_headers(self: *Request) *Headers {
return &self.headers;
}
@@ -249,50 +261,6 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise {
}
const testing = @import("../../testing.zig");
test "fetch: request" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let request = new Request('flower.png')", "undefined" },
.{ "request.url", "https://lightpanda.io/flower.png" },
.{ "request.method", "GET" },
}, .{});
try runner.testCases(&.{
.{ "let request2 = new Request('https://google.com', { method: 'POST', body: 'Hello, World' })", "undefined" },
.{ "request2.url", "https://google.com" },
.{ "request2.method", "POST" },
}, .{});
}
test "fetch: Browser.fetch" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ var ok = false;
\\ const request = new Request("http://127.0.0.1:9582/loader");
\\ fetch(request).then((response) => { ok = response.ok; });
\\ false;
,
"false",
},
// all events have been resolved.
.{ "ok", "true" },
}, .{});
try runner.testCases(&.{
.{
\\ var ok2 = false;
\\ const request2 = new Request("http://127.0.0.1:9582/loader");
\\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }());
\\ false;
,
"false",
},
// all events have been resolved.
.{ "ok2", "true" },
}, .{});
test "fetch: Request" {
try testing.htmlRunner("fetch/request.html");
}

View File

@@ -36,7 +36,8 @@ const Page = @import("../page.zig").Page;
// https://developer.mozilla.org/en-US/docs/Web/API/Response
const Response = @This();
status: u16 = 0,
status: u16 = 200,
status_text: []const u8 = "",
headers: Headers,
mime: ?Mime = null,
url: []const u8 = "",
@@ -50,7 +51,7 @@ const ResponseBody = union(enum) {
const ResponseOptions = struct {
status: u16 = 200,
statusText: []const u8 = "",
statusText: ?[]const u8 = null,
headers: ?HeadersInit = null,
};
@@ -72,10 +73,13 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
};
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
return .{
.body = body,
.headers = headers,
.status = options.status,
.status_text = status_text,
};
}
@@ -105,6 +109,10 @@ pub fn get_status(self: *const Response) u16 {
return self.status;
}
pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text;
}
pub fn get_url(self: *const Response) []const u8 {
return self.url;
}
@@ -183,9 +191,6 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise {
}
const testing = @import("../../testing.zig");
test "fetch: response" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io" });
defer runner.deinit();
try runner.testCases(&.{}, .{});
test "fetch: Response" {
try testing.htmlRunner("fetch/response.html");
}

View File

@@ -36,9 +36,9 @@ const Response = @import("Response.zig");
pub const Interfaces = .{
@import("Headers.zig"),
@import("Headers.zig").HeaderEntryIterator,
@import("Headers.zig").HeaderKeyIterator,
@import("Headers.zig").HeaderValueIterator,
@import("Headers.zig").HeadersEntryIterable,
@import("Headers.zig").HeadersKeyIterable,
@import("Headers.zig").HeadersValueIterable,
@import("Request.zig"),
@import("Response.zig"),
};

View File

@@ -0,0 +1,102 @@
<script src="../testing.js"></script>
<script id=headers>
let headers = new Headers({"Set-Cookie": "name=world"});
testing.expectEqual("name=world", headers.get("set-cookie"));
let myHeaders = new Headers();
myHeaders.append("Content-Type", "image/jpeg"),
testing.expectEqual(false, myHeaders.has("Picture-Type"));
testing.expectEqual("image/jpeg", myHeaders.get("Content-Type"));
myHeaders.append("Content-Type", "image/png");
testing.expectEqual("image/jpeg, image/png", myHeaders.get("Content-Type"));
myHeaders.delete("Content-Type");
testing.expectEqual(null, myHeaders.get("Content-Type"));
myHeaders.set("Picture-Type", "image/svg")
testing.expectEqual("image/svg", myHeaders.get("Picture-Type"));
testing.expectEqual(true, myHeaders.has("Picture-Type"))
const originalHeaders = new Headers([["Content-Type", "application/json"], ["Authorization", "Bearer token123"]]);
testing.expectEqual("application/json", originalHeaders.get("Content-Type"));
testing.expectEqual("Bearer token123", originalHeaders.get("Authorization"));
const newHeaders = new Headers(originalHeaders);
testing.expectEqual("application/json", newHeaders.get("Content-Type"));
testing.expectEqual("Bearer token123" ,newHeaders.get("Authorization"));
testing.expectEqual(true ,newHeaders.has("Content-Type"));
testing.expectEqual(true ,newHeaders.has("Authorization"));
testing.expectEqual(false, newHeaders.has("X-Custom"));
newHeaders.set("X-Custom", "test-value");
testing.expectEqual("test-value", newHeaders.get("X-Custom"));
testing.expectEqual(null, originalHeaders.get("X-Custom"));
testing.expectEqual(false, originalHeaders.has("X-Custom"));
</script>
<script id=keys>
const testKeyHeaders = new Headers();
testKeyHeaders.set("Content-Type", "application/json");
testKeyHeaders.set("Authorization", "Bearer token123");
testKeyHeaders.set("X-Custom", "test-value");
const keys = [];
for (const key of testKeyHeaders.keys()) {
keys.push(key);
}
testing.expectEqual(3, keys.length);
testing.expectEqual(true, keys.includes("Content-Type"));
testing.expectEqual(true, keys.includes("Authorization"));
testing.expectEqual(true, keys.includes("X-Custom"));
</script>
<script id=values>
const testValuesHeaders = new Headers();
testValuesHeaders.set("Content-Type", "application/json");
testValuesHeaders.set("Authorization", "Bearer token123");
testValuesHeaders.set("X-Custom", "test-value");
const values = [];
for (const value of testValuesHeaders.values()) {
values.push(value);
}
testing.expectEqual(3, values.length);
testing.expectEqual(true, values.includes("application/json"));
testing.expectEqual(true, values.includes("Bearer token123"));
testing.expectEqual(true, values.includes("test-value"));
</script>
<script id=entries>
const testEntriesHeaders = new Headers();
testEntriesHeaders.set("Content-Type", "application/json");
testEntriesHeaders.set("Authorization", "Bearer token123");
testEntriesHeaders.set("X-Custom", "test-value");
const entries = [];
for (const entry of testEntriesHeaders.entries()) {
entries.push(entry);
}
testing.expectEqual(3, entries.length);
const entryMap = new Map(entries);
testing.expectEqual("application/json", entryMap.get("Content-Type"));
testing.expectEqual("Bearer token123", entryMap.get("Authorization"));
testing.expectEqual("test-value", entryMap.get("X-Custom"));
const entryKeys = Array.from(entryMap.keys());
testing.expectEqual(3, entryKeys.length);
testing.expectEqual(true, entryKeys.includes("Content-Type"));
testing.expectEqual(true, entryKeys.includes("Authorization"));
testing.expectEqual(true, entryKeys.includes("X-Custom"));
const entryValues = Array.from(entryMap.values());
testing.expectEqual(3, entryValues.length);
testing.expectEqual(true, entryValues.includes("application/json"));
testing.expectEqual(true, entryValues.includes("Bearer token123"));
testing.expectEqual(true, entryValues.includes("test-value"))
</script>

View File

@@ -0,0 +1,22 @@
<script src="../testing.js"></script>
<script id=request>
let request = new Request("flower.png");
testing.expectEqual("http://localhost:9582/src/tests/fetch/flower.png", request.url);
testing.expectEqual("GET", request.method);
let request2 = new Request("https://google.com", {
method: "POST",
body: "Hello, World",
cache: "reload",
credentials: "omit",
headers: { "Sender": "me", "Target": "you" }
}
);
testing.expectEqual("https://google.com", request2.url);
testing.expectEqual("POST", request2.method);
testing.expectEqual("omit", request2.credentials);
testing.expectEqual("reload", request2.cache);
testing.expectEqual("me", request2.headers.get("SeNdEr"));
testing.expectEqual("you", request2.headers.get("target"));
</script>

View File

@@ -0,0 +1,38 @@
<script src="../testing.js"></script>
<script id=response>
let response = new Response("Hello, World!");
testing.expectEqual(200, response.status);
testing.expectEqual("", response.statusText);
testing.expectEqual(true, response.ok);
testing.expectEqual("", response.url);
testing.expectEqual(false, response.redirected);
let response2 = new Response("Error occurred", {
status: 404,
statusText: "Not Found",
headers: {
"Content-Type": "text/plain",
"X-Custom": "test-value",
"Cache-Control": "no-cache"
}
});
testing.expectEqual(404, response2.status);
testing.expectEqual("Not Found", response2.statusText);
testing.expectEqual(false, response2.ok);
testing.expectEqual("text/plain", response2.headers.get("Content-Type"));
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
let response3 = new Response("Created", { status: 201, statusText: "Created" });
testing.expectEqual(201, response3.status);
testing.expectEqual("Created", response3.statusText);
testing.expectEqual(true, response3.ok);
let nullResponse = new Response(null);
testing.expectEqual(200, nullResponse.status);
testing.expectEqual("", nullResponse.statusText);
let emptyResponse = new Response("");
testing.expectEqual(200, emptyResponse.status);
</script>