From 9e4db89521728c61b17ce6e3f45b2429862f2318 Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 13:04:12 +0100 Subject: [PATCH 1/3] add window stubs, HTMLElement hidden/tabIndex, input attribute reflections - Window: alert/confirm/prompt (no-op stubs), devicePixelRatio - HTMLElement: hidden (boolean), tabIndex (integer) - Input: placeholder, min, max, step, multiple, autocomplete --- .../tests/element/html/htmlelement-props.html | 39 +++++++++ .../tests/element/html/input-attrs.html | 87 +++++++++++++++++++ src/browser/tests/window-stubs.html | 34 ++++++++ src/browser/webapi/Window.zig | 16 ++++ src/browser/webapi/element/Html.zig | 29 +++++++ src/browser/webapi/element/html/Input.zig | 59 +++++++++++++ 6 files changed, 264 insertions(+) create mode 100644 src/browser/tests/element/html/htmlelement-props.html create mode 100644 src/browser/tests/element/html/input-attrs.html create mode 100644 src/browser/tests/window-stubs.html diff --git a/src/browser/tests/element/html/htmlelement-props.html b/src/browser/tests/element/html/htmlelement-props.html new file mode 100644 index 00000000..d8f8c905 --- /dev/null +++ b/src/browser/tests/element/html/htmlelement-props.html @@ -0,0 +1,39 @@ + + + + +
Visible div
+ +
No tabindex
+ + + + diff --git a/src/browser/tests/element/html/input-attrs.html b/src/browser/tests/element/html/input-attrs.html new file mode 100644 index 00000000..3e3bf606 --- /dev/null +++ b/src/browser/tests/element/html/input-attrs.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/window-stubs.html b/src/browser/tests/window-stubs.html new file mode 100644 index 00000000..6b6f375b --- /dev/null +++ b/src/browser/tests/window-stubs.html @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 6e184b21..898ba71e 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -704,6 +704,15 @@ fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global { }; } +// Headless browser stubs: alert/confirm/prompt are no-ops +fn jsAlert(_: *Window, _: ?[]const u8) void {} +fn jsConfirm(_: *Window, _: ?[]const u8) bool { + return false; +} +fn jsPrompt(_: *Window, _: ?[]const u8, _: ?[]const u8) ?[]const u8 { + return null; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Window); @@ -775,12 +784,19 @@ pub const JsApi = struct { pub const innerWidth = bridge.property(1920, .{ .template = false }); pub const innerHeight = bridge.property(1080, .{ .template = false }); + pub const devicePixelRatio = bridge.property(1, .{ .template = false }); + // This should return a window-like object in specific conditions. Would be // pretty complicated to properly support I think. pub const opener = bridge.property(null, .{ .template = false }); + + pub const alert = bridge.function(Window.jsAlert, .{}); + pub const confirm = bridge.function(Window.jsConfirm, .{}); + pub const prompt = bridge.function(Window.jsPrompt, .{}); }; const testing = @import("../../testing.zig"); test "WebApi: Window" { try testing.htmlRunner("window", .{}); + try testing.htmlRunner("window-stubs.html", .{}); } diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 43417bdb..7639d204 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -342,6 +342,29 @@ pub fn click(self: *HtmlElement, page: *Page) !void { try page._event_manager.dispatch(self.asEventTarget(), event); } +pub fn getHidden(self: *HtmlElement) bool { + return self.asElement().getAttributeSafe(comptime .wrap("hidden")) != null; +} + +pub fn setHidden(self: *HtmlElement, hidden: bool, page: *Page) !void { + if (hidden) { + try self.asElement().setAttributeSafe(comptime .wrap("hidden"), .wrap(""), page); + } else { + try self.asElement().removeAttribute(comptime .wrap("hidden"), page); + } +} + +pub fn getTabIndex(self: *HtmlElement) i32 { + const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse return -1; + return std.fmt.parseInt(i32, attr, 10) catch -1; +} + +pub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void { + var buf: [12]u8 = undefined; + const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch return; + try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page); +} + fn getAttributeFunction( self: *HtmlElement, listener_type: GlobalEventHandler, @@ -1151,6 +1174,9 @@ pub const JsApi = struct { pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true }); pub const click = bridge.function(HtmlElement.click, .{}); + pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{}); + pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{}); + pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{}); pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{}); pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{}); @@ -1281,3 +1307,6 @@ const testing = @import("../../../testing.zig"); test "WebApi: HTML.event_listeners" { try testing.htmlRunner("element/html/event_listeners.html", .{}); } +test "WebApi: HTMLElement.props" { + try testing.htmlRunner("element/html/htmlelement-props.html", .{}); +} diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 7ab4db6b..fcbb05bb 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -287,6 +287,58 @@ pub fn setRequired(self: *Input, required: bool, page: *Page) !void { } } +pub fn getPlaceholder(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe(comptime .wrap("placeholder")) orelse ""; +} + +pub fn setPlaceholder(self: *Input, placeholder: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("placeholder"), .wrap(placeholder), page); +} + +pub fn getMin(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe(comptime .wrap("min")) orelse ""; +} + +pub fn setMin(self: *Input, min: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("min"), .wrap(min), page); +} + +pub fn getMax(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe(comptime .wrap("max")) orelse ""; +} + +pub fn setMax(self: *Input, max: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("max"), .wrap(max), page); +} + +pub fn getStep(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe(comptime .wrap("step")) orelse ""; +} + +pub fn setStep(self: *Input, step: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("step"), .wrap(step), page); +} + +pub fn getMultiple(self: *const Input) bool { + return self.asConstElement().getAttributeSafe(comptime .wrap("multiple")) != null; +} + +pub fn setMultiple(self: *Input, multiple: bool, page: *Page) !void { + if (multiple) { + try self.asElement().setAttributeSafe(comptime .wrap("multiple"), .wrap(""), page); + } else { + try self.asElement().removeAttribute(comptime .wrap("multiple"), page); + } +} + +pub fn getAutocomplete(self: *const Input) []const u8 { + return self.asConstElement().getAttributeSafe(comptime .wrap("autocomplete")) orelse ""; +} + +pub fn setAutocomplete(self: *Input, autocomplete: []const u8, page: *Page) !void { + try self.asElement().setAttributeSafe(comptime .wrap("autocomplete"), .wrap(autocomplete), page); +} + pub fn select(self: *Input, page: *Page) !void { const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0; try self.setSelectionRange(0, len, null, page); @@ -564,6 +616,12 @@ pub const JsApi = struct { pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{}); pub const form = bridge.accessor(Input.getForm, null, .{}); pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{}); + pub const placeholder = bridge.accessor(Input.getPlaceholder, Input.setPlaceholder, .{}); + pub const min = bridge.accessor(Input.getMin, Input.setMin, .{}); + pub const max = bridge.accessor(Input.getMax, Input.setMax, .{}); + pub const step = bridge.accessor(Input.getStep, Input.setStep, .{}); + pub const multiple = bridge.accessor(Input.getMultiple, Input.setMultiple, .{}); + pub const autocomplete = bridge.accessor(Input.getAutocomplete, Input.setAutocomplete, .{}); pub const select = bridge.function(Input.select, .{}); pub const selectionStart = bridge.accessor(Input.getSelectionStart, Input.setSelectionStart, .{}); @@ -662,4 +720,5 @@ test "WebApi: HTML.Input" { try testing.htmlRunner("element/html/input.html", .{}); try testing.htmlRunner("element/html/input_click.html", .{}); try testing.htmlRunner("element/html/input_radio.html", .{}); + try testing.htmlRunner("element/html/input-attrs.html", .{}); } From 07a87dfba7f5da7819202847f6db71dafd6d217b Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 13:13:12 +0100 Subject: [PATCH 2/3] fix tabIndex default for interactive elements per spec interactive elements (input, button, a, select, textarea, iframe) default to 0 when tabindex attribute is absent; others default to -1. also add TODO for hidden "until-found" tristate. --- .../tests/element/html/htmlelement-props.html | 17 +++++++++++++++++ src/browser/webapi/element/Html.zig | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/browser/tests/element/html/htmlelement-props.html b/src/browser/tests/element/html/htmlelement-props.html index d8f8c905..a008adc5 100644 --- a/src/browser/tests/element/html/htmlelement-props.html +++ b/src/browser/tests/element/html/htmlelement-props.html @@ -30,10 +30,27 @@ i1.tabIndex = 10; testing.expectEqual(10, i1.tabIndex); + // Non-interactive elements default to -1 const d3 = document.getElementById('d3'); testing.expectEqual(-1, d3.tabIndex); d3.tabIndex = 0; testing.expectEqual(0, d3.tabIndex); + + // Interactive elements default to 0 per spec + const input = document.createElement('input'); + testing.expectEqual(0, input.tabIndex); + + const button = document.createElement('button'); + testing.expectEqual(0, button.tabIndex); + + const a = document.createElement('a'); + testing.expectEqual(0, a.tabIndex); + + const select = document.createElement('select'); + testing.expectEqual(0, select.tabIndex); + + const textarea = document.createElement('textarea'); + testing.expectEqual(0, textarea.tabIndex); } diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 7639d204..ba6361d3 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -342,6 +342,8 @@ pub fn click(self: *HtmlElement, page: *Page) !void { try page._event_manager.dispatch(self.asEventTarget(), event); } +// TODO: Per spec, hidden is a tristate: true | false | "until-found". +// We only support boolean for now; "until-found" would need bridge union support. pub fn getHidden(self: *HtmlElement) bool { return self.asElement().getAttributeSafe(comptime .wrap("hidden")) != null; } @@ -355,7 +357,13 @@ pub fn setHidden(self: *HtmlElement, hidden: bool, page: *Page) !void { } pub fn getTabIndex(self: *HtmlElement) i32 { - const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse return -1; + const attr = self.asElement().getAttributeSafe(comptime .wrap("tabindex")) orelse { + // Per spec, interactive/focusable elements default to 0 when tabindex is absent + return switch (self._type) { + .anchor, .area, .button, .input, .select, .textarea, .iframe => 0, + else => -1, + }; + }; return std.fmt.parseInt(i32, attr, 10) catch -1; } From c28afbf19309c10937a9984e95b131a9ae498a6b Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 15:40:59 +0100 Subject: [PATCH 3/3] address review feedback: move stubs test, inline bridge functions, catch unreachable --- .../{window-stubs.html => window/stubs.html} | 2 +- src/browser/webapi/Window.zig | 26 ++++++++++--------- src/browser/webapi/element/Html.zig | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) rename src/browser/tests/{window-stubs.html => window/stubs.html} (95%) diff --git a/src/browser/tests/window-stubs.html b/src/browser/tests/window/stubs.html similarity index 95% rename from src/browser/tests/window-stubs.html rename to src/browser/tests/window/stubs.html index 6b6f375b..74c8a944 100644 --- a/src/browser/tests/window-stubs.html +++ b/src/browser/tests/window/stubs.html @@ -1,5 +1,5 @@ - +