From 3d8b1abda4f3870adc811089f8966488f677514b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 11 Dec 2025 16:45:19 +0800 Subject: [PATCH] More legacy tests Largely around how URL attributes (a.href, img.href, link.href) handle empty values. --- Makefile | 4 +- src/browser/tests/document/focus.html | 9 + src/browser/tests/element/html/anchor.html | 215 +++++++++++++++++++-- src/browser/tests/element/html/image.html | 12 +- src/browser/tests/element/html/link.html | 12 ++ src/browser/tests/legacy/html/element.html | 4 +- src/browser/webapi/Element.zig | 8 +- src/browser/webapi/element/html/Anchor.zig | 118 ++++++++--- src/browser/webapi/element/html/Image.zig | 12 +- src/browser/webapi/element/html/Link.zig | 15 +- 10 files changed, 353 insertions(+), 56 deletions(-) create mode 100644 src/browser/tests/element/html/link.html diff --git a/Makefile b/Makefile index 7208b9ee..3f79e1fa 100644 --- a/Makefile +++ b/Makefile @@ -99,11 +99,11 @@ wpt-summary: ## Test - `grep` is used to filter out the huge compile command on build ifeq ($(OS), macos) test: - @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ + @script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' 2>&1 \ | grep --line-buffered -v "^/.*zig test -freference-trace" else test: - @script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \ + @script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace' /dev/null 2>&1 \ | grep --line-buffered -v "^/.*zig test -freference-trace" endif diff --git a/src/browser/tests/document/focus.html b/src/browser/tests/document/focus.html index 5b7b7c07..3e72e1d4 100644 --- a/src/browser/tests/document/focus.html +++ b/src/browser/tests/document/focus.html @@ -79,3 +79,12 @@ testing.expectEqual(1, focusCount); } + + + diff --git a/src/browser/tests/element/html/anchor.html b/src/browser/tests/element/html/anchor.html index a1402688..2eaa7935 100644 --- a/src/browser/tests/element/html/anchor.html +++ b/src/browser/tests/element/html/anchor.html @@ -1,29 +1,93 @@ + + - - - OK - + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index 5ad6454d..b9cb8153 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -31,9 +31,19 @@ testing.expectEqual('', img.alt); img.src = 'test.png'; - testing.expectEqual('test.png', img.src); + // src property returns resolved absolute URL + testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.png', img.src); + // getAttribute returns the raw attribute value testing.expectEqual('test.png', img.getAttribute('src')); + img.src = '/absolute/path.png'; + testing.expectEqual('http://127.0.0.1:9582/absolute/path.png', img.src); + testing.expectEqual('/absolute/path.png', img.getAttribute('src')); + + img.src = 'https://example.com/image.png'; + testing.expectEqual('https://example.com/image.png', img.src); + testing.expectEqual('https://example.com/image.png', img.getAttribute('src')); + img.alt = 'Test image'; testing.expectEqual('Test image', img.alt); testing.expectEqual('Test image', img.getAttribute('alt')); diff --git a/src/browser/tests/element/html/link.html b/src/browser/tests/element/html/link.html new file mode 100644 index 00000000..25fd5430 --- /dev/null +++ b/src/browser/tests/element/html/link.html @@ -0,0 +1,12 @@ + + + + diff --git a/src/browser/tests/legacy/html/element.html b/src/browser/tests/legacy/html/element.html index 4de1f058..d1701ae3 100644 --- a/src/browser/tests/legacy/html/element.html +++ b/src/browser/tests/legacy/html/element.html @@ -32,7 +32,7 @@ testing.expectEqual('', a.href); testing.expectEqual('', a.host); a.href = 'about'; - testing.expectEqual('http://localhost:9582/src/tests/html/about', a.href); + testing.expectEqual('http://localhost:9589/html/about', a.href); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 00b46cdf..8d722be9 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -613,13 +613,17 @@ pub fn focus(self: *Element, page: *Page) !void { const Event = @import("Event.zig"); if (page.document._active_element) |old| { - if (old == self) return; + if (old == self) { + return; + } const blur_event = try Event.init("blur", null, page); try page._event_manager.dispatch(old.asEventTarget(), blur_event); } - page.document._active_element = self; + if (self.asNode().isConnected()) { + page.document._active_element = self; + } const focus_event = try Event.init("focus", null, page); try page._event_manager.dispatch(self.asEventTarget(), focus_event); diff --git a/src/browser/webapi/element/html/Anchor.zig b/src/browser/webapi/element/html/Anchor.zig index d6b85c46..006843db 100644 --- a/src/browser/webapi/element/html/Anchor.zig +++ b/src/browser/webapi/element/html/Anchor.zig @@ -40,17 +40,11 @@ pub fn asNode(self: *Anchor) *Node { pub fn getHref(self: *Anchor, page: *Page) ![]const u8 { const element = self.asElement(); - const href = element.getAttributeSafe("href") orelse ""; + const href = element.getAttributeSafe("href") orelse return ""; if (href.len == 0) { - return page.url; + return ""; } - - const first = href[0]; - if (first == '#' or first == '?' or first == '/' or std.mem.startsWith(u8, href, "../") or std.mem.startsWith(u8, href, "./")) { - return URL.resolve(page.call_arena, page.url, href, .{}); - } - - return href; + return URL.resolve(page.call_arena, page.url, href, .{}); } pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void { @@ -66,17 +60,30 @@ pub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return (try URL.getOrigin(page.call_arena, href)) orelse "null"; } pub fn getHost(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); - return URL.getHost(href); + const href = try getResolvedHref(self, page) orelse return ""; + const host = URL.getHost(href); + const protocol = URL.getProtocol(href); + const port = URL.getPort(href); + + // Strip default ports + if (port.len > 0) { + if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or + (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80"))) + { + return URL.getHostname(href); + } + } + + return host; } pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const pathname = URL.getPathname(href); const search = URL.getSearch(href); @@ -101,12 +108,12 @@ pub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getHostname(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getHostname(href); } pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const current_port = URL.getPort(href); const new_host = if (current_port.len > 0) try std.fmt.allocPrint(page.call_arena, "{s}:{s}", .{ value, current_port }) @@ -117,12 +124,24 @@ pub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getPort(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); - return URL.getPort(href); + const href = try getResolvedHref(self, page) orelse return ""; + const port = URL.getPort(href); + const protocol = URL.getProtocol(href); + + // Return empty string for default ports + if (port.len > 0) { + if ((std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443")) or + (std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80"))) + { + return ""; + } + } + + return port; } pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const hostname = URL.getHostname(href); const protocol = URL.getProtocol(href); @@ -145,12 +164,12 @@ pub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void { } pub fn getSearch(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getSearch(href); } pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const host = URL.getHost(href); const pathname = URL.getPathname(href); @@ -167,12 +186,12 @@ pub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void { } pub fn getHash(self: *Anchor, page: *Page) ![]const u8 { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return ""; return URL.getHash(href); } pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { - const href = try getResolvedHref(self, page); + const href = try getResolvedHref(self, page) orelse return; const protocol = URL.getProtocol(href); const host = URL.getHost(href); const pathname = URL.getPathname(href); @@ -188,6 +207,50 @@ pub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void { try setHref(self, new_href, page); } +pub fn getPathname(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page) orelse return ""; + return URL.getPathname(href); +} + +pub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page) orelse return; + const protocol = URL.getProtocol(href); + const host = URL.getHost(href); + const search = URL.getSearch(href); + const hash = URL.getHash(href); + + // Add / prefix if not present and value is not empty + const pathname = if (value.len > 0 and value[0] != '/') + try std.fmt.allocPrint(page.call_arena, "/{s}", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + +pub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 { + const href = try getResolvedHref(self, page) orelse return ""; + return URL.getProtocol(href); +} + +pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void { + const href = try getResolvedHref(self, page) orelse return; + const host = URL.getHost(href); + const pathname = URL.getPathname(href); + const search = URL.getSearch(href); + const hash = URL.getHash(href); + + // Add : suffix if not present + const protocol = if (value.len > 0 and value[value.len - 1] != ':') + try std.fmt.allocPrint(page.call_arena, "{s}:", .{value}) + else + value; + + const new_href = try buildUrl(page.call_arena, protocol, host, pathname, search, hash); + try setHref(self, new_href, page); +} + pub fn getType(self: *Anchor) []const u8 { return self.asElement().getAttributeSafe("type") orelse ""; } @@ -212,9 +275,12 @@ pub fn setText(self: *Anchor, value: []const u8, page: *Page) !void { try self.asNode().setTextContent(value, page); } -fn getResolvedHref(self: *Anchor, page: *Page) ![:0]const u8 { - const href = self.asElement().getAttributeSafe("href"); - return URL.resolve(page.call_arena, page.url, href orelse "", .{}); +fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 { + const href = self.asElement().getAttributeSafe("href") orelse return null; + if (href.len == 0) { + return null; + } + return try URL.resolve(page.call_arena, page.url, href, .{}); } // Helper function to build a new URL from components @@ -248,9 +314,11 @@ pub const JsApi = struct { pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{}); pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{}); pub const origin = bridge.accessor(Anchor.getOrigin, null, .{}); + pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{}); pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{}); pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{}); pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{}); + pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{}); pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{}); pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 9576fde7..affaaba0 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -1,6 +1,7 @@ const std = @import("std"); const js = @import("../../../js/js.zig"); const Page = @import("../../../Page.zig"); +const URL = @import("../../../URL.zig"); const Node = @import("../../Node.zig"); const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); @@ -33,8 +34,15 @@ pub fn asNode(self: *Image) *Node { return self.asElement().asNode(); } -pub fn getSrc(self: *const Image) []const u8 { - return self.asConstElement().getAttributeSafe("src") orelse ""; +pub fn getSrc(self: *const Image, page: *Page) ![]const u8 { + const element = self.asConstElement(); + const src = element.getAttributeSafe("src") orelse return ""; + if (src.len == 0) { + return ""; + } + + // Always resolve the src against the page URL + return URL.resolve(page.call_arena, page.url, src, .{}); } pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index b9db1e53..e381e227 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -35,8 +35,14 @@ pub fn asNode(self: *Link) *Node { } pub fn getHref(self: *Link, page: *Page) ![]const u8 { - const href = self.asElement().getAttributeSafe("href"); - return URL.resolve(page.call_arena, page.url, href orelse "", .{}); + const element = self.asElement(); + const href = element.getAttributeSafe("href") orelse return ""; + if (href.len == 0) { + return ""; + } + + // Always resolve the href against the page URL + return URL.resolve(page.call_arena, page.url, href, .{}); } pub fn setHref(self: *Link, value: []const u8, page: *Page) !void { @@ -63,3 +69,8 @@ pub const JsApi = struct { pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: HTML.Link" { + try testing.htmlRunner("element/html/link.html", .{}); +}