More legacy tests

Largely around how URL attributes (a.href, img.href, link.href) handle empty
values.
This commit is contained in:
Karl Seguin
2025-12-11 16:45:19 +08:00
parent 0b141e44ae
commit 3d8b1abda4
10 changed files with 353 additions and 56 deletions

View File

@@ -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

View File

@@ -79,3 +79,12 @@
testing.expectEqual(1, focusCount);
}
</script>
<script id="focus_disconnected">
{
const focused = document.activeElement;
document.createElement('a').focus();
testing.expectEqual(focused, document.activeElement);
}
</script>

View File

@@ -1,29 +1,93 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!-- Test anchors with various href values -->
<a id=a0></a>
<a href="../anchor1.html" id=a1></a>
<a href="/hello/world/anchor2.html" id=a2></a>
<a href="https://www.openmymind.net/Elixirs-With-Statement/" id=a3></a>
<script id=href>
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/anchor.html', $('#a0').href)
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/anchor1.html', $('#a1').href)
testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href)
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href)
</script>
<a id=link href=foo>OK</a>
<script id=anchor>
let link = $('#link');
<script id=empty_href>
testing.expectEqual('', $('#a0').href);
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/anchor1.html', $('#a1').href);
testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href);
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', $('#link').href);
</script>
<script id=dynamic_anchor_defaults>
{
let a = document.createElement('a');
testing.expectEqual('', a.href);
testing.expectEqual('', a.host);
testing.expectEqual('', a.hostname);
testing.expectEqual('', a.port);
testing.expectEqual('', a.pathname);
testing.expectEqual('', a.search);
testing.expectEqual('', a.hash);
testing.expectEqual('', a.origin);
testing.expectEqual('', a.target);
testing.expectEqual('', a.type);
testing.expectEqual('', a.text);
}
</script>
<script id=dynamic_anchor_empty_href_behavior>
{
let a = document.createElement('a');
a.search = 'q=test';
testing.expectEqual('', a.href);
testing.expectEqual('', a.search);
a.hash = 'section';
testing.expectEqual('', a.href);
testing.expectEqual('', a.hash);
a.port = '8080';
testing.expectEqual('', a.href);
testing.expectEqual('', a.port);
a.hostname = 'example.com';
testing.expectEqual('', a.href);
testing.expectEqual('', a.hostname);
a.host = 'example.com:9000';
testing.expectEqual('', a.href);
testing.expectEqual('', a.host);
}
</script>
<script id=dynamic_anchor_with_href>
{
let a = document.createElement('a');
a.href = 'https://lightpanda.io/';
testing.expectEqual('https://lightpanda.io/', a.href);
testing.expectEqual('lightpanda.io', a.host);
testing.expectEqual('lightpanda.io', a.hostname);
testing.expectEqual('', a.port);
testing.expectEqual('/', a.pathname);
testing.expectEqual('', a.search);
testing.expectEqual('', a.hash);
testing.expectEqual('https://lightpanda.io', a.origin);
}
</script>
<script id=anchor_url_manipulation>
{
let link = document.createElement('a');
link.href = 'https://lightpanda.io/';
testing.expectEqual('', link.target);
link.target = '_blank';
testing.expectEqual('_blank', link.target);
link.target = '';
testing.expectEqual('foo', link.href);
link.href = 'https://lightpanda.io/';
testing.expectEqual('https://lightpanda.io/', link.href);
testing.expectEqual('', link.target);
testing.expectEqual('https://lightpanda.io', link.origin);
@@ -31,15 +95,16 @@
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
testing.expectEqual('https://lightpanda.io/', link.href);
link.host = 'lightpanda.io';
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('lightpanda.io', link.hostname);
link.hostname = 'foo.bar';
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar/', link.href);
testing.expectEqual('', link.search);
@@ -54,19 +119,129 @@
testing.expectEqual('', link.port);
link.port = '443';
testing.expectEqual('', link.port);
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null;
testing.expectEqual('', link.port);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
testing.expectEqual('foo', link.href = 'foo');
link.href = 'foo';
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', link.href);
testing.expectEqual('', link.type);
link.type = 'text/html';
testing.expectEqual('text/html', link.type);
testing.expectEqual('OK', link.text);
link.text = 'foo';
testing.expectEqual('foo', link.text);
testing.expectEqual('', link.text);
link.text = 'Click here';
testing.expectEqual('Click here', link.text);
}
</script>
<script id=anchor_port_non_default>
{
let a = document.createElement('a');
a.href = 'https://example.com:8443/path';
testing.expectEqual('example.com:8443', a.host);
testing.expectEqual('example.com', a.hostname);
testing.expectEqual('8443', a.port);
testing.expectEqual('https://example.com:8443/path', a.href);
a.href = 'http://example.com:8080/path';
testing.expectEqual('example.com:8080', a.host);
testing.expectEqual('8080', a.port);
a.href = 'http://example.com:80/path';
testing.expectEqual('example.com', a.host);
testing.expectEqual('', a.port);
}
</script>
<script id=anchor_special_chars>
{
let a = document.createElement('a');
a.href = 'https://example.com/';
a.search = '?test=1';
testing.expectEqual('?test=1', a.search);
a.search = 'test=2';
testing.expectEqual('?test=2', a.search);
a.hash = '#section';
testing.expectEqual('#section', a.hash);
a.hash = 'other';
testing.expectEqual('#other', a.hash);
a.search = '';
testing.expectEqual('', a.search);
testing.expectEqual('https://example.com/#other', a.href);
a.hash = '';
testing.expectEqual('', a.hash);
testing.expectEqual('https://example.com/', a.href);
}
</script>
<script id=anchor_name_attribute>
{
let a = document.createElement('a');
testing.expectEqual('', a.name);
a.name = 'myanchor';
testing.expectEqual('myanchor', a.name);
a.name = '';
testing.expectEqual('', a.name);
}
</script>
<script id=anchor_pathname>
{
let a = document.createElement('a');
a.href = 'https://example.com/path/to/page';
testing.expectEqual('/path/to/page', a.pathname);
a.pathname = '/new/path';
testing.expectEqual('/new/path', a.pathname);
testing.expectEqual('https://example.com/new/path', a.href);
a.pathname = 'another';
testing.expectEqual('/another', a.pathname);
testing.expectEqual('https://example.com/another', a.href);
}
</script>
<script id=anchor_protocol>
{
let a = document.createElement('a');
a.href = 'https://example.com/';
testing.expectEqual('https:', a.protocol);
a.protocol = 'http:';
testing.expectEqual('http:', a.protocol);
testing.expectEqual('http://example.com/', a.href);
a.protocol = 'https';
testing.expectEqual('https:', a.protocol);
testing.expectEqual('https://example.com/', a.href);
}
</script>
<script id=toString>
{
let a = document.createElement('a');
a.href = 'https://example.com/test';
testing.expectEqual('https://example.com/test', a.toString());
let b = document.createElement('a');
testing.expectEqual('', b.toString());
}
</script>

View File

@@ -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'));

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=link>
let l2 = document.createElement('link');
testing.expectEqual('', l2.href);
l2.href = 'https://lightpanda.io/opensource-browser/15';
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
l2.href = '/over/9000';
testing.expectEqual('http://127.0.0.1:9582/over/9000', l2.href);
</script>

View File

@@ -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);
</script>
<script id=focus>
@@ -49,5 +49,5 @@
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
l2.href = '/over/9000';
testing.expectEqual('http://localhost:9582/over/9000', l2.href);
testing.expectEqual('http://localhost:9589/over/9000', l2.href);
</script>

View File

@@ -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);

View File

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

View File

@@ -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 {

View File

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