From 275b97948b0aab6f086767339edeb738e37e5c43 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Wed, 28 May 2025 16:25:52 +0200 Subject: [PATCH 1/3] input element properties --- src/browser/html/elements.zig | 79 ++++++++++++++++ src/browser/netsurf.zig | 171 ++++++++++++++++++++++++++++++++-- 2 files changed, 242 insertions(+), 8 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 652e60c4..a3567089 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -626,6 +626,85 @@ pub const HTMLInputElement = struct { pub const Self = parser.Input; pub const prototype = *HTMLElement; pub const subtype = .node; + + pub fn get_defaultValue(self: *parser.Input) ![]const u8 { + return try parser.inputGetDefaultValue(self); + } + pub fn set_defaultValue(self: *parser.Input, default_value: []const u8) !void { + try parser.inputSetDefaultValue(self, default_value); + } + pub fn get_defaultChecked(self: *parser.Input) !bool { + return try parser.inputGetDefaultChecked(self); + } + pub fn set_defaultChecked(self: *parser.Input, default_checked: bool) !void { + try parser.inputSetDefaultChecked(self, default_checked); + } + pub fn get_from(self: *parser.Input) !?*parser.Form { + return try parser.inputGetForm(self); + } + pub fn get_accept(self: *parser.Input) ![]const u8 { + return try parser.inputGetAccept(self); + } + pub fn set_accept(self: *parser.Input, accept: []const u8) !void { + try parser.inputSetAccept(self, accept); + } + pub fn get_alt(self: *parser.Input) ![]const u8 { + return try parser.inputGetAlt(self); + } + pub fn set_alt(self: *parser.Input, alt: []const u8) !void { + try parser.inputSetAlt(self, alt); + } + pub fn get_checked(self: *parser.Input) !bool { + return try parser.inputGetChecked(self); + } + pub fn set_checked(self: *parser.Input, checked: bool) !void { + try parser.inputSetChecked(self, checked); + } + pub fn get_disabled(self: *parser.Input) !bool { + return try parser.inputGetDisabled(self); + } + pub fn set_disabled(self: *parser.Input, disabled: bool) !void { + try parser.inputSetDisabled(self, disabled); + } + pub fn get_maxLength(self: *parser.Input) !i32 { + return try parser.inputGetMaxLength(self); + } + pub fn set_maxLength(self: *parser.Input, max_length: u32) !void { + try parser.inputSetMaxLength(self, max_length); + } + pub fn get_name(self: *parser.Input) ![]const u8 { + return try parser.inputGetName(self); + } + pub fn set_name(self: *parser.Input, name: []const u8) !void { + try parser.inputSetName(self, name); + } + pub fn get_readOnly(self: *parser.Input) !bool { + return try parser.inputGetReadOnly(self); + } + pub fn set_readOnly(self: *parser.Input, read_only: bool) !void { + try parser.inputSetReadOnly(self, read_only); + } + pub fn get_size(self: *parser.Input) !u32 { + return try parser.inputGetSize(self); + } + pub fn set_size(self: *parser.Input, size: u32) !void { + try parser.inputSetSize(self, size); + } + pub fn get_src(self: *parser.Input) ![]const u8 { + return try parser.inputGetSrc(self); + } + pub fn set_src(self: *parser.Input, src: []const u8) !void { + try parser.inputSetSrc(self, src); + } + pub fn get_type(self: *parser.Input) ![]const u8 { + return try parser.inputGetType(self); + } + pub fn get_value(self: *parser.Input) ![]const u8 { + return try parser.inputGetValue(self); + } + pub fn set_value(self: *parser.Input, value: []const u8) !void { + try parser.inputSetValue(self, value); + } }; pub const HTMLLIElement = struct { diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 036e825e..e571ca79 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -2498,14 +2498,6 @@ pub fn optionSetSelected(option: *Option, selected: bool) !void { try DOMErr(err); } -// Input -pub fn inputGetChecked(input: *Input) !bool { - var b: bool = false; - const err = c.dom_html_input_element_get_checked(input, &b); - try DOMErr(err); - return b; -} - // HtmlCollection pub fn htmlCollectionGetLength(collection: *HTMLCollection) !u32 { var len: u32 = 0; @@ -2601,3 +2593,166 @@ pub fn imageSetIsMap(image: *Image, is_map: bool) !void { const err = c.dom_html_image_element_set_is_map(image, is_map); try DOMErr(err); } + +// Input +// - Input.align is deprecated +// - Input.useMap is deprecated +// - HTMLElement.access_key +// - HTMLElement.tabIndex +// TODO methods: +// - HTMLElement.blur +// - HTMLElement.focus +// - select +// - HTMLElement.click + +pub fn inputGetDefaultValue(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_default_value(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetDefaultValue(input: *Input, default_value: []const u8) !void { + const err = c.dom_html_input_element_set_default_value(input, try strFromData(default_value)); + try DOMErr(err); +} + +pub fn inputGetDefaultChecked(input: *Input) !bool { + var default_checked: bool = false; + const err = c.dom_html_input_element_get_default_checked(input, &default_checked); + try DOMErr(err); + return default_checked; +} +pub fn inputSetDefaultChecked(input: *Input, default_checked: bool) !void { + const err = c.dom_html_input_element_set_default_checked(input, default_checked); + try DOMErr(err); +} + +pub fn inputGetForm(input: *Input) !?*Form { + var form: ?*Form = null; + const err = c.dom_html_input_element_get_form(input, &form); + try DOMErr(err); + return form; +} + +pub fn inputGetAccept(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_accept(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetAccept(input: *Input, accept: []const u8) !void { + const err = c.dom_html_input_element_set_accept(input, try strFromData(accept)); + try DOMErr(err); +} + +pub fn inputGetAlt(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_alt(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetAlt(input: *Input, alt: []const u8) !void { + const err = c.dom_html_input_element_set_alt(input, try strFromData(alt)); + try DOMErr(err); +} + +pub fn inputGetChecked(input: *Input) !bool { + var checked: bool = false; + const err = c.dom_html_input_element_get_checked(input, &checked); + try DOMErr(err); + return checked; +} +pub fn inputSetChecked(input: *Input, checked: bool) !void { + const err = c.dom_html_input_element_set_checked(input, checked); + try DOMErr(err); +} + +pub fn inputGetDisabled(input: *Input) !bool { + var disabled: bool = false; + const err = c.dom_html_input_element_get_disabled(input, &disabled); + try DOMErr(err); + return disabled; +} +pub fn inputSetDisabled(input: *Input, disabled: bool) !void { + const err = c.dom_html_input_element_set_disabled(input, disabled); + try DOMErr(err); +} + +pub fn inputGetMaxLength(input: *Input) !i32 { + var max_length: i32 = 0; + const err = c.dom_html_input_element_get_max_length(input, &max_length); + try DOMErr(err); + return max_length; +} +pub fn inputSetMaxLength(input: *Input, max_length: u32) !void { + const err = c.dom_html_input_element_set_max_length(input, max_length); + try DOMErr(err); +} + +pub fn inputGetName(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_name(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetName(input: *Input, name: []const u8) !void { + const err = c.dom_html_input_element_set_name(input, try strFromData(name)); + try DOMErr(err); +} +pub fn inputGetReadOnly(input: *Input) !bool { + var read_only: bool = false; + const err = c.dom_html_input_element_get_read_only(input, &read_only); + try DOMErr(err); + return read_only; +} +pub fn inputSetReadOnly(input: *Input, read_only: bool) !void { + const err = c.dom_html_input_element_set_read_only(input, read_only); + try DOMErr(err); +} +pub fn inputGetSize(input: *Input) !u32 { + var size: u32 = 0; + const err = c.dom_html_input_element_get_size(input, &size); + try DOMErr(err); + if (size == ulongNegativeOne) return 20; // 20 + return size; +} +pub fn inputSetSize(input: *Input, size: u32) !void { + const err = c.dom_html_input_element_set_size(input, size); + try DOMErr(err); +} + +pub fn inputGetSrc(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_src(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetSrc(input: *Input, src: []const u8) !void { + const err = c.dom_html_input_element_set_src(input, try strFromData(src)); + try DOMErr(err); +} + +pub fn inputGetType(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_type(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} + +pub fn inputGetValue(input: *Input) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_input_element_get_value(input, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} +pub fn inputSetValue(input: *Input, value: []const u8) !void { + const err = c.dom_html_input_element_set_value(input, try strFromData(value)); + try DOMErr(err); +} From 19d40845a4336624bdef4e6c3195a157c7beffcb Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Sat, 31 May 2025 02:33:09 +0200 Subject: [PATCH 2/3] input prop testing --- src/browser/html/elements.zig | 156 ++++++++++++++++++++++++++++++++-- src/browser/netsurf.zig | 27 ++++-- src/url.zig | 15 ++++ vendor/netsurf/libdom | 2 +- 4 files changed, 189 insertions(+), 11 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index a3567089..7b45325a 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -22,6 +22,7 @@ const generate = @import("../../runtime/generate.zig"); const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; +const urlStitch = @import("../../url.zig").URL.stitch; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; const Element = @import("../dom/element.zig").Element; @@ -639,7 +640,7 @@ pub const HTMLInputElement = struct { pub fn set_defaultChecked(self: *parser.Input, default_checked: bool) !void { try parser.inputSetDefaultChecked(self, default_checked); } - pub fn get_from(self: *parser.Input) !?*parser.Form { + pub fn get_form(self: *parser.Input) !?*parser.Form { return try parser.inputGetForm(self); } pub fn get_accept(self: *parser.Input) ![]const u8 { @@ -669,7 +670,7 @@ pub const HTMLInputElement = struct { pub fn get_maxLength(self: *parser.Input) !i32 { return try parser.inputGetMaxLength(self); } - pub fn set_maxLength(self: *parser.Input, max_length: u32) !void { + pub fn set_maxLength(self: *parser.Input, max_length: i32) !void { try parser.inputSetMaxLength(self, max_length); } pub fn get_name(self: *parser.Input) ![]const u8 { @@ -687,18 +688,22 @@ pub const HTMLInputElement = struct { pub fn get_size(self: *parser.Input) !u32 { return try parser.inputGetSize(self); } - pub fn set_size(self: *parser.Input, size: u32) !void { + pub fn set_size(self: *parser.Input, size: i32) !void { try parser.inputSetSize(self, size); } pub fn get_src(self: *parser.Input) ![]const u8 { return try parser.inputGetSrc(self); } - pub fn set_src(self: *parser.Input, src: []const u8) !void { - try parser.inputSetSrc(self, src); + pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void { + const new_src = try urlStitch(page.call_arena, src, page.url.raw); + try parser.inputSetSrc(self, new_src); } pub fn get_type(self: *parser.Input) ![]const u8 { return try parser.inputGetType(self); } + pub fn set_type(self: *parser.Input, type_: []const u8) !void { + try parser.inputSetType(self, type_); + } pub fn get_value(self: *parser.Input) ![]const u8 { return try parser.inputGetValue(self); } @@ -1261,3 +1266,144 @@ test "Browser.HTML.Element" { .{ "a.href", "https://lightpanda.io/opensource-browser/about" }, }, .{}); } +test "Browser.HTML.Element.propeties" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" }); + defer runner.deinit(); + const bool_valids = [_]Valid{ + .{ .input = "true", .is_str = false }, + .{ .input = "", .is_str = true, .expected = "false" }, + .{ .input = "13.5", .is_str = true, .expected = "true" }, + }; + const str_valids = [_]Valid{ + .{ .input = "foo", .is_str = true }, + .{ .input = "5", .is_str = false, .expected = "5" }, + .{ .input = "", .is_str = true }, + .{ .input = "document", .is_str = false, .expected = "[object HTMLDocument]" }, + }; + // TODO these tests are mostly just data should we store them in Sqlite or so? + try testCreateElement(&runner, "input"); + // Valid input.form is tested separately :Browser.HTML.Element.propeties.input.form + try testProperty(&runner, "input", "form", "null", "null", &.{}, &.{.{ .input = "foo", .is_str = true }}); + try testProperty(&runner, "input", "accept", "", "", &str_valids, &.{}); + try testProperty(&runner, "input", "alt", "", "", &str_valids, &.{}); + try testProperty(&runner, "input", "disabled", "false", "false", &bool_valids, &.{}); + try testProperty(&runner, "input", "maxLength", "-1", "0", &.{.{ .input = "5", .is_str = false }}, &.{.{ .input = "banana", .is_str = true }}); + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.maxLength = -45", null }}, .{})); + try testProperty(&runner, "input", "name", "", "", &str_valids, &.{}); + try testProperty(&runner, "input", "readOnly", "false", "false", &bool_valids, &.{}); + try testProperty(&runner, "input", "size", "20", "20", &.{.{ .input = "5", .is_str = false }}, &.{.{ .input = "-26", .is_str = false }}); + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 0", null }}, .{})); + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 'banana'", null }}, .{})); + try testProperty(&runner, "input", "src", "", "", &.{ + .{ .input = "foo", .is_str = true, .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20 + .{ .input = "-3", .is_str = false, .expected = "https://lightpanda.io/-3" }, + .{ .input = "", .is_str = true, .expected = "https://lightpanda.io/noslashattheend" }, + }, &.{}); + try testProperty(&runner, "input", "type", "text", "text", &.{.{ .input = "checkbox", .is_str = true }}, &.{.{ .input = "5", .is_str = true }}); + + // Properties that are related + try runner.testCases(&.{ + .{ "let input_checked = document.createElement('input')", null }, + .{ "input_checked.defaultChecked", "false" }, + .{ "input_checked.checked", "false" }, + + .{ "input_checked.defaultChecked = true", "true" }, + .{ "input_checked.defaultChecked", "true" }, + .{ "input_checked.checked", "true" }, // Also perceived as true + + .{ "input_checked.checked = false", "false" }, + .{ "input_checked.defaultChecked", "true" }, + .{ "input_checked.checked", "false" }, + + .{ "input_checked.defaultChecked = true", "true" }, + .{ "input_checked.checked", "false" }, // Still false + }, .{}); + try runner.testCases(&.{ + .{ "let input_value = document.createElement('input')", null }, + .{ "input_value.defaultValue", "" }, + .{ "input_value.value", "" }, + + .{ "input_value.defaultValue = 3.1", "3.1" }, + .{ "input_value.defaultValue", "3.1" }, + .{ "input_value.value", "3.1" }, // Also perceived as 3.1 + + .{ "input_value.value = 'mango'", "mango" }, + .{ "input_value.defaultValue", "3.1" }, + .{ "input_value.value", "mango" }, + + .{ "input_value.defaultValue = true", "true" }, + .{ "input_value.value", "mango" }, // Still mango + }, .{}); +} +test "Browser.HTML.Element.propeties.input.form" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = + \\
+ }); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let elem_input = document.querySelector('input')", null }, + }, .{}); + try testProperty(&runner, "input", "form", "[object HTMLFormElement]", "[object HTMLFormElement]", &.{}, &.{.{ .input = "5", .is_str = false }}); +} + +const Valid = struct { + input: []const u8, + is_str: bool, + expected: ?[]const u8 = null, // Needed when input != expected +}; +const Invalid = struct { + input: []const u8, + is_str: bool, +}; + +fn testCreateElement(runner: *testing.JsRunner, comptime name: []const u8) !void { + try runner.testCases(&.{ + .{ "let elem_" ++ name ++ " = document.createElement('" ++ name ++ "')", null }, + }, .{}); +} +// TODO reduce comptime +// Default is the expected value after creation and after setting an invalid value +// Valid input is expected to return itself or the expected value +// Invalid input is expected to return the default value +// .{ "elem.type", "text" }, // default +// .{ "elem.type = 'checkbox'", "checkbox" }, // valid +// .{ "elem.type", "checkbox" }, +// .{ "elem.type = '5'", "5" }, // invalid +// .{ "elem.type", "text" }, +fn testProperty( + runner: *testing.JsRunner, + comptime name: []const u8, + comptime property: []const u8, + comptime initial: []const u8, + comptime default: []const u8, + comptime valids: []const Valid, + comptime invalids: []const Invalid, +) !void { + const elem_dot_prop = "elem_" ++ name ++ "." ++ property; + + try runner.testCases(&.{ + .{ elem_dot_prop, initial }, + }, .{}); + + inline for (valids) |valid| { + const set_input = if (valid.is_str) "'" ++ valid.input ++ "'" else valid.input; + const expected = valid.expected orelse valid.input; + try runner.testCases(&.{ + .{ elem_dot_prop ++ " = " ++ set_input, null }, + .{ elem_dot_prop, expected }, + }, .{}); + } + + inline for (invalids) |invalid| { + const set_input = if (invalid.is_str) "'" ++ invalid.input ++ "'" else invalid.input; + try runner.testCases(&.{ + .{ elem_dot_prop ++ " = " ++ set_input, null }, + .{ elem_dot_prop, default }, + }, .{}); + } +} diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index e571ca79..a1f12e01 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -2687,8 +2687,9 @@ pub fn inputGetMaxLength(input: *Input) !i32 { try DOMErr(err); return max_length; } -pub fn inputSetMaxLength(input: *Input, max_length: u32) !void { - const err = c.dom_html_input_element_set_max_length(input, max_length); +pub fn inputSetMaxLength(input: *Input, max_length: i32) !void { + if (max_length < 0) return error.NegativeValueNotAllowed; + const err = c.dom_html_input_element_set_max_length(input, @intCast(max_length)); try DOMErr(err); } @@ -2720,8 +2721,10 @@ pub fn inputGetSize(input: *Input) !u32 { if (size == ulongNegativeOne) return 20; // 20 return size; } -pub fn inputSetSize(input: *Input, size: u32) !void { - const err = c.dom_html_input_element_set_size(input, size); +pub fn inputSetSize(input: *Input, size: i32) !void { + if (size == 0) return error.ZeroNotAllowed; + const new_size = if (size < 0) 20 else size; + const err = c.dom_html_input_element_set_size(input, @intCast(new_size)); try DOMErr(err); } @@ -2732,6 +2735,7 @@ pub fn inputGetSrc(input: *Input) ![]const u8 { const s = s_ orelse return ""; return strToData(s); } +// url should already be stitched! pub fn inputSetSrc(input: *Input, src: []const u8) !void { const err = c.dom_html_input_element_set_src(input, try strFromData(src)); try DOMErr(err); @@ -2741,9 +2745,22 @@ pub fn inputGetType(input: *Input) ![]const u8 { var s_: ?*String = null; const err = c.dom_html_input_element_get_type(input, &s_); try DOMErr(err); - const s = s_ orelse return ""; + const s = s_ orelse return "text"; return strToData(s); } +pub fn inputSetType(input: *Input, type_: []const u8) !void { + // @speed sort values by usage frequency/length + const possible_values = [_][]const u8{ "text", "search", "tel", "url", "email", "password", "date", "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", "file", "hidden", "image", "button", "submit", "reset" }; + var found = false; + for (possible_values) |item| { + if (std.mem.eql(u8, type_, item)) { + found = true; + break; + } + } + const new_type = if (found) type_ else "text"; + try elementSetAttribute(@ptrCast(input), "type", new_type); +} pub fn inputGetValue(input: *Input) ![]const u8 { var s_: ?*String = null; diff --git a/src/url.zig b/src/url.zig index b8e54b02..565a31a1 100644 --- a/src/url.zig +++ b/src/url.zig @@ -110,6 +110,11 @@ pub const URL = struct { } return src; } + if (src.len == 0) { + if (opts.alloc == .always) { + return allocator.dupe(u8, base); + } + } const protocol_end: usize = blk: { if (std.mem.indexOf(u8, base, "://")) |protocol_index| { @@ -256,6 +261,16 @@ test "URL: Stiching src as full path" { try testing.expectString("https://lightpanda.io/something.js", result); } +test "URL: Stitching Base & Src URLs (empty src)" { + const allocator = testing.allocator; + + const base = "https://www.google.com/xyz/abc/123"; + const src = ""; + const result = try URL.stitch(allocator, src, base); + defer allocator.free(result); + try testing.expectString("https://www.google.com/xyz/abc/123", result); +} + test "URL: concatQueryString" { defer testing.reset(); const arena = testing.arena_allocator; diff --git a/vendor/netsurf/libdom b/vendor/netsurf/libdom index f8fc2170..9a1d1e41 160000 --- a/vendor/netsurf/libdom +++ b/vendor/netsurf/libdom @@ -1 +1 @@ -Subproject commit f8fc21702b5003f4c0ca73b14a7b9db54a1a393c +Subproject commit 9a1d1e41d1109ae8784667b37bc591642b60c0f6 From ceb94530064ee8c088160cbf829c1d1ff94afb41 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:04:31 +0200 Subject: [PATCH 3/3] Simplify testing --- src/browser/html/elements.zig | 154 ++++++++++++++++------------------ src/url.zig | 3 +- 2 files changed, 76 insertions(+), 81 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 7b45325a..aefd78b4 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -196,8 +196,7 @@ pub const HTMLAnchorElement = struct { } pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { - const stitch = @import("../../url.zig").stitch; - const full = try stitch(page.call_arena, href, page.url.raw, .{}); + const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); return try parser.anchorSetHref(self, full); } @@ -695,7 +694,7 @@ pub const HTMLInputElement = struct { return try parser.inputGetSrc(self); } pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void { - const new_src = try urlStitch(page.call_arena, src, page.url.raw); + const new_src = try urlStitch(page.call_arena, src, page.url.raw, .{ .alloc = .if_needed }); try parser.inputSetSrc(self, new_src); } pub fn get_type(self: *parser.Input) ![]const u8 { @@ -1266,40 +1265,55 @@ test "Browser.HTML.Element" { .{ "a.href", "https://lightpanda.io/opensource-browser/about" }, }, .{}); } -test "Browser.HTML.Element.propeties" { +test "Browser.HTML.HtmlInputElement.propeties" { var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" }); defer runner.deinit(); - const bool_valids = [_]Valid{ - .{ .input = "true", .is_str = false }, - .{ .input = "", .is_str = true, .expected = "false" }, - .{ .input = "13.5", .is_str = true, .expected = "true" }, - }; - const str_valids = [_]Valid{ - .{ .input = "foo", .is_str = true }, - .{ .input = "5", .is_str = false, .expected = "5" }, - .{ .input = "", .is_str = true }, - .{ .input = "document", .is_str = false, .expected = "[object HTMLDocument]" }, - }; - // TODO these tests are mostly just data should we store them in Sqlite or so? - try testCreateElement(&runner, "input"); - // Valid input.form is tested separately :Browser.HTML.Element.propeties.input.form - try testProperty(&runner, "input", "form", "null", "null", &.{}, &.{.{ .input = "foo", .is_str = true }}); - try testProperty(&runner, "input", "accept", "", "", &str_valids, &.{}); - try testProperty(&runner, "input", "alt", "", "", &str_valids, &.{}); - try testProperty(&runner, "input", "disabled", "false", "false", &bool_valids, &.{}); - try testProperty(&runner, "input", "maxLength", "-1", "0", &.{.{ .input = "5", .is_str = false }}, &.{.{ .input = "banana", .is_str = true }}); - try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.maxLength = -45", null }}, .{})); - try testProperty(&runner, "input", "name", "", "", &str_valids, &.{}); - try testProperty(&runner, "input", "readOnly", "false", "false", &bool_valids, &.{}); - try testProperty(&runner, "input", "size", "20", "20", &.{.{ .input = "5", .is_str = false }}, &.{.{ .input = "-26", .is_str = false }}); - try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 0", null }}, .{})); - try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 'banana'", null }}, .{})); - try testProperty(&runner, "input", "src", "", "", &.{ - .{ .input = "foo", .is_str = true, .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20 - .{ .input = "-3", .is_str = false, .expected = "https://lightpanda.io/-3" }, - .{ .input = "", .is_str = true, .expected = "https://lightpanda.io/noslashattheend" }, - }, &.{}); - try testProperty(&runner, "input", "type", "text", "text", &.{.{ .input = "checkbox", .is_str = true }}, &.{.{ .input = "5", .is_str = true }}); + var alloc = std.heap.ArenaAllocator.init(runner.app.allocator); + defer alloc.deinit(); + const arena = alloc.allocator(); + + try runner.testCases(&.{.{ "let elem_input = document.createElement('input')", null }}, .{}); + + try runner.testCases(&.{.{ "elem_input.form", "null" }}, .{}); // Initial value + // Valid input.form is tested separately :Browser.HTML.HtmlInputElement.propeties.form + try testProperty(arena, &runner, "elem_input.form", "null", &.{.{ .input = "'foo'" }}); // Invalid + + try runner.testCases(&.{.{ "elem_input.accept", "" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.accept", null, &str_valids); // Valid + + try runner.testCases(&.{.{ "elem_input.alt", "" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.alt", null, &str_valids); // Valid + + try runner.testCases(&.{.{ "elem_input.disabled", "false" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.disabled", null, &bool_valids); // Valid + + try runner.testCases(&.{.{ "elem_input.maxLength", "-1" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.maxLength", null, &.{.{ .input = "5" }}); // Valid + try testProperty(arena, &runner, "elem_input.maxLength", "0", &.{.{ .input = "'banana'" }}); // Invalid + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.maxLength = -45", null }}, .{})); // Error + + try runner.testCases(&.{.{ "elem_input.name", "" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.name", null, &str_valids); // Valid + + try runner.testCases(&.{.{ "elem_input.readOnly", "false" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.readOnly", null, &bool_valids); // Valid + + try runner.testCases(&.{.{ "elem_input.size", "20" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.size", null, &.{.{ .input = "5" }}); // Valid + try testProperty(arena, &runner, "elem_input.size", "20", &.{.{ .input = "-26" }}); // Invalid + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 0", null }}, .{})); // Error + try testing.expectError(error.ExecutionError, runner.testCases(&.{.{ "elem_input.size = 'banana'", null }}, .{})); // Error + + try runner.testCases(&.{.{ "elem_input.src", "" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.src", null, &.{ + .{ .input = "'foo'", .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20 + .{ .input = "-3", .expected = "https://lightpanda.io/-3" }, + .{ .input = "''", .expected = "https://lightpanda.io/noslashattheend" }, + }); + + try runner.testCases(&.{.{ "elem_input.type", "text" }}, .{}); // Initial value + try testProperty(arena, &runner, "elem_input.type", null, &.{.{ .input = "'checkbox'", .expected = "checkbox" }}); // Valid + try testProperty(arena, &runner, "elem_input.type", "text", &.{.{ .input = "'5'" }}); // Invalid // Properties that are related try runner.testCases(&.{ @@ -1335,7 +1349,7 @@ test "Browser.HTML.Element.propeties" { .{ "input_value.value", "mango" }, // Still mango }, .{}); } -test "Browser.HTML.Element.propeties.input.form" { +test "Browser.HTML.HtmlInputElement.propeties.form" { var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = \\