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", .{});
+}