diff --git a/src/browser/env.zig b/src/browser/env.zig index 3dfad2fc..e45ff75d 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -62,9 +62,9 @@ pub const SessionState = struct { // exists for the entire rendering of the page call_arena: std.mem.Allocator = undefined, - pub fn getNodeWrapper(self: *SessionState, comptime T: type, node: *parser.Node) !*T { - if (parser.nodeGetEmbedderData(node)) |wrap| { - return @alignCast(@ptrCast(wrap)); + pub fn getOrCreateNodeWrapper(self: *SessionState, comptime T: type, node: *parser.Node) !*T { + if (try self.getNodeWrapper(T, node)) |wrap| { + return wrap; } const wrap = try self.arena.create(T); @@ -73,4 +73,11 @@ pub const SessionState = struct { parser.nodeSetEmbedderData(node, wrap); return wrap; } + + pub fn getNodeWrapper(_: *SessionState, comptime T: type, node: *parser.Node) !?*T { + if (parser.nodeGetEmbedderData(node)) |wrap| { + return @alignCast(@ptrCast(wrap)); + } + return null; + } }; diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index c5f6f572..7f9b64c8 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -187,7 +187,7 @@ pub const HTMLDocument = struct { } pub fn get_readyState(node: *parser.DocumentHTML, state: *SessionState) ![]const u8 { - const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(node)); + const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node)); return @tagName(self.ready_state); } @@ -266,7 +266,7 @@ pub const HTMLDocument = struct { } pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, state: *SessionState) !void { - const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(html_doc)); + const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); self.ready_state = .interactive; const evt = try parser.eventCreate(); @@ -277,7 +277,7 @@ pub const HTMLDocument = struct { } pub fn documentIsComplete(html_doc: *parser.DocumentHTML, state: *SessionState) !void { - const self = try state.getNodeWrapper(HTMLDocument, @ptrCast(html_doc)); + const self = try state.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc)); self.ready_state = .complete; } }; diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 71d43793..b5f62f8a 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -47,7 +47,6 @@ pub const Interfaces = .{ HTMLEmbedElement, HTMLFieldSetElement, HTMLFontElement, - HTMLFormElement, HTMLFrameElement, HTMLFrameSetElement, HTMLHRElement, @@ -77,7 +76,6 @@ pub const Interfaces = .{ HTMLProgressElement, HTMLQuoteElement, HTMLScriptElement, - HTMLSelectElement, HTMLSourceElement, HTMLSpanElement, HTMLStyleElement, @@ -95,6 +93,9 @@ pub const Interfaces = .{ HTMLUListElement, HTMLVideoElement, CSSProperties, + + @import("form.zig").HTMLFormElement, + @import("select.zig").HTMLSelectElement, }; pub const Union = generate.Union(Interfaces); @@ -516,12 +517,6 @@ pub const HTMLFontElement = struct { pub const subtype = .node; }; -pub const HTMLFormElement = struct { - pub const Self = parser.Form; - pub const prototype = *HTMLElement; - pub const subtype = .node; -}; - pub const HTMLFrameElement = struct { pub const Self = parser.Frame; pub const prototype = *HTMLElement; @@ -806,12 +801,6 @@ pub const HTMLScriptElement = struct { } }; -pub const HTMLSelectElement = struct { - pub const Self = parser.Select; - pub const prototype = *HTMLElement; - pub const subtype = .node; -}; - pub const HTMLSourceElement = struct { pub const Self = parser.Source; pub const prototype = *HTMLElement; diff --git a/src/browser/html/form.zig b/src/browser/html/form.zig new file mode 100644 index 00000000..3cb169d5 --- /dev/null +++ b/src/browser/html/form.zig @@ -0,0 +1,54 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const parser = @import("../netsurf.zig"); +const HTMLElement = @import("elements.zig").HTMLElement; +const FormData = @import("../xhr/form_data.zig").FormData; + +pub const HTMLFormElement = struct { + pub const Self = parser.Form; + pub const prototype = *HTMLElement; + pub const subtype = .node; + + pub fn _requestSubmit(self: *parser.Form) !void { + try parser.formElementSubmit(self); + } + + pub fn _reset(self: *parser.Form) !void { + try parser.formElementReset(self); + } +}; + +pub const Submission = struct { + method: ?[]const u8, + form_data: FormData, +}; + +pub fn processSubmission(arena: Allocator, form: *parser.Form) !?Submission { + const form_element: *parser.Element = @ptrCast(form); + const method = try parser.elementGetAttribute(form_element, "method"); + + return .{ + .method = method, + .form_data = try FormData.fromForm(arena, form), + }; +} + +// Check xhr/form_data.zig for tests diff --git a/src/browser/html/select.zig b/src/browser/html/select.zig new file mode 100644 index 00000000..f07e5d64 --- /dev/null +++ b/src/browser/html/select.zig @@ -0,0 +1,158 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +const std = @import("std"); + +const parser = @import("../netsurf.zig"); +const HTMLElement = @import("elements.zig").HTMLElement; +const SessionState = @import("../env.zig").SessionState; + +pub const HTMLSelectElement = struct { + pub const Self = parser.Select; + pub const prototype = *HTMLElement; + pub const subtype = .node; + + // By default, if no option is explicitly selected, the first option should + // be selected. However, libdom doesn't do this, and it sets the + // selectedIndex to -1, which is a valid value for "nothing selected". + // Therefore, when libdom says the selectedIndex == -1, we don't know if + // it means that nothing is selected, or if the first option is selected by + // default. + // There are cases where this won't work, but when selectedIndex is + // explicitly set, we set this boolean flag. Then, when we're getting then + // selectedIndex, if this flag is == false, which is to say that if + // selectedIndex hasn't been explicitly set AND if we have at least 1 option + // AND if it isn't a multi select, we can make the 1st item selected by + // default (by returning selectedIndex == 0). + explicit_index_set: bool = false, + + pub fn get_length(select: *parser.Select) !u32 { + return parser.selectGetLength(select); + } + + pub fn get_form(select: *parser.Select) !?*parser.Form { + return parser.selectGetForm(select); + } + + pub fn get_name(select: *parser.Select) ![]const u8 { + return parser.selectGetName(select); + } + pub fn set_name(select: *parser.Select, name: []const u8) !void { + return parser.selectSetName(select, name); + } + + pub fn get_disabled(select: *parser.Select) !bool { + return parser.selectGetDisabled(select); + } + pub fn set_disabled(select: *parser.Select, disabled: bool) !void { + return parser.selectSetDisabled(select, disabled); + } + + pub fn get_multiple(select: *parser.Select) !bool { + return parser.selectGetMultiple(select); + } + pub fn set_multiple(select: *parser.Select, multiple: bool) !void { + return parser.selectSetMultiple(select, multiple); + } + + pub fn get_selectedIndex(select: *parser.Select, state: *SessionState) !i32 { + const self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); + const selected_index = try parser.selectGetSelectedIndex(select); + + // See the explicit_index_set field documentation + if (!self.explicit_index_set) { + if (selected_index == -1) { + if (try parser.selectGetMultiple(select) == false) { + if (try get_length(select) > 0) { + return 0; + } + } + } + } + return selected_index; + } + + // Libdom's dom_html_select_select_set_selected_index will crash if index + // is out of range, and it doesn't properly unset options + pub fn set_selectedIndex(select: *parser.Select, index: i32, state: *SessionState) !void { + var self = try state.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); + self.explicit_index_set = true; + + const options = try parser.selectGetOptions(select); + const len = try parser.optionCollectionGetLength(options); + for (0..len) |i| { + const option = try parser.optionCollectionItem(options, @intCast(i)); + try parser.optionSetSelected(option, false); + } + if (index >= 0 and index < try get_length(select)) { + const option = try parser.optionCollectionItem(options, @intCast(index)); + try parser.optionSetSelected(option, true); + } + } +}; + +const testing = @import("../../testing.zig"); +test "Browser.HTML.Select" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = + \\
+ \\ + \\
+ \\ + }); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "const s = document.getElementById('s1');", null }, + .{ "s.form", "[object HTMLFormElement]" }, + + .{ "document.getElementById('s2').form", "null" }, + + .{ "s.disabled", "false" }, + .{ "s.disabled = true", null }, + .{ "s.disabled", "true" }, + .{ "s.disabled = false", null }, + .{ "s.disabled", "false" }, + + .{ "s.multiple", "false" }, + .{ "s.multiple = true", null }, + .{ "s.multiple", "true" }, + .{ "s.multiple = false", null }, + .{ "s.multiple", "false" }, + + .{ "s.name;", "s1" }, + .{ "s.name = 'sel1';", null }, + .{ "s.name", "sel1" }, + + .{ "s.length;", "2" }, + + .{ "s.selectedIndex", "0" }, + .{ "s.selectedIndex = 2", null }, // out of range + .{ "s.selectedIndex", "-1" }, + + .{ "s.selectedIndex = -1", null }, + .{ "s.selectedIndex", "-1" }, + + .{ "s.selectedIndex = 0", null }, + .{ "s.selectedIndex", "0" }, + + .{ "s.selectedIndex = 1", null }, + .{ "s.selectedIndex", "1" }, + + .{ "s.selectedIndex = -323", null }, + .{ "s.selectedIndex", "-1" }, + }, .{}); +} diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 0bd630e3..7a4d8056 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1303,6 +1303,15 @@ pub inline fn nodeToDocument(node: *Node) *Document { return @as(*Document, @ptrCast(node)); } +// Combination of nodeToElement + elementHTMLGetTagType +pub fn nodeHTMLGetTagType(node: *Node) !?Tag { + if (try nodeType(node) != .element) { + return null; + } + const html_element: *ElementHTML = @ptrCast(node); + return try elementHTMLGetTagType(html_element); +} + // CharacterData pub const CharacterData = c.dom_characterdata; @@ -1818,6 +1827,8 @@ pub const Title = c.dom_html_title_element; pub const Track = struct { base: *c.dom_html_element }; pub const UList = c.dom_html_u_list_element; pub const Video = struct { base: *c.dom_html_element }; +pub const HTMLCollection = c.dom_html_collection; +pub const OptionCollection = c.dom_html_options_collection; // Document Fragment pub const DocumentFragment = c.dom_document_fragment; @@ -2341,3 +2352,160 @@ pub fn documentHTMLGetLocation(T: type, doc: *DocumentHTML) !?*T { pub fn validateName(name: []const u8) !bool { return c._dom_validate_name(try strFromData(name)); } + +// Form +pub fn formElementSubmit(form: *Form) !void { + const err = c.dom_html_form_element_submit(form); + try DOMErr(err); +} + +pub fn formElementReset(form: *Form) !void { + const err = c.dom_html_form_element_reset(form); + try DOMErr(err); +} + +pub fn formGetCollection(form: *Form) !*HTMLCollection { + var collection: ?*HTMLCollection = null; + const err = c.dom_html_form_element_get_elements(form, &collection); + try DOMErr(err); + return collection.?; +} + +// TextArea +pub fn textareaGetValue(textarea: *TextArea) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_text_area_element_get_value(textarea, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} + +// Select +pub fn selectGetOptions(select: *Select) !*OptionCollection { + var collection: ?*OptionCollection = null; + const err = c.dom__html_select_element_get_options(select, &collection); + try DOMErr(err); + return collection.?; +} + +pub fn selectGetDisabled(select: *Select) !bool { + var disabled: bool = false; + const err = c.dom_html_select_element_get_disabled(select, &disabled); + try DOMErr(err); + return disabled; +} + +pub fn selectSetDisabled(select: *Select, disabled: bool) !void { + const err = c.dom_html_select_element_set_disabled(select, disabled); + try DOMErr(err); +} + +pub fn selectGetMultiple(select: *Select) !bool { + var multiple: bool = false; + const err = c.dom_html_select_element_get_multiple(select, &multiple); + try DOMErr(err); + return multiple; +} + +pub fn selectSetMultiple(select: *Select, multiple: bool) !void { + const err = c.dom_html_select_element_set_multiple(select, multiple); + try DOMErr(err); +} + +pub fn selectGetName(select: *Select) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_select_element_get_name(select, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} + +pub fn selectSetName(select: *Select, name: []const u8) !void { + const err = c.dom_html_select_element_set_name(select, try strFromData(name)); + try DOMErr(err); +} + +pub fn selectGetLength(select: *Select) !u32 { + var length: u32 = 0; + const err = c.dom_html_select_element_get_length(select, &length); + try DOMErr(err); + return length; +} + +pub fn selectGetSelectedIndex(select: *Select) !i32 { + var index: i32 = 0; + const err = c.dom_html_select_element_get_selected_index(select, &index); + try DOMErr(err); + return index; +} + +pub fn selectSetSelectedIndex(select: *Select, index: i32) !void { + const err = c.dom_html_select_element_set_selected_index(select, index); + try DOMErr(err); +} + +pub fn selectGetForm(select: *Select) !?*Form { + var form: ?*Form = null; + const err = c.dom_html_select_element_get_form(select, &form); + try DOMErr(err); + return form; +} + +// OptionCollection +pub fn optionCollectionGetLength(collection: *OptionCollection) !u32 { + var len: u32 = 0; + const err = c.dom_html_options_collection_get_length(collection, &len); + try DOMErr(err); + return len; +} + +pub fn optionCollectionItem(collection: *OptionCollection, index: u32) !*Option { + var node: ?*NodeExternal = undefined; + const err = c.dom_html_options_collection_item(collection, index, &node); + try DOMErr(err); + return @ptrCast(node.?); +} + +// Option +pub fn optionGetValue(option: *Option) ![]const u8 { + var s_: ?*String = null; + const err = c.dom_html_option_element_get_value(option, &s_); + try DOMErr(err); + const s = s_ orelse return ""; + return strToData(s); +} + +pub fn optionGetSelected(option: *Option) !bool { + var selected: bool = false; + const err = c.dom_html_option_element_get_selected(option, &selected); + try DOMErr(err); + return selected; +} + +pub fn optionSetSelected(option: *Option, selected: bool) !void { + const err = c.dom_html_option_element_set_selected(option, selected); + 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; + const err = c.dom_html_collection_get_length(collection, &len); + try DOMErr(err); + return len; +} + +pub fn htmlCollectionItem(collection: *HTMLCollection, index: u32) !*Node { + var node: ?*NodeExternal = undefined; + const err = c.dom_html_collection_item(collection, index, &node); + try DOMErr(err); + return @ptrCast(node.?); +} diff --git a/src/browser/page.zig b/src/browser/page.zig index f34604f3..b3597f44 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -515,14 +515,9 @@ pub const Page = struct { fn _windowClicked(self: *Page, event: *parser.Event) !void { const target = (try parser.eventTarget(event)) orelse return; - const node = parser.eventTargetToNode(target); - if (try parser.nodeType(node) != .element) { - return; - } - - const html_element: *parser.ElementHTML = @ptrCast(node); - switch (try parser.elementHTMLGetTagType(html_element)) { + const tag = (try parser.nodeHTMLGetTagType(node)) orelse return; + switch (tag) { .a => { const element: *parser.Element = @ptrCast(node); const href = (try parser.elementGetAttribute(element, "href")) orelse return; diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index f2127d6f..076c0596 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -20,9 +20,12 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const parser = @import("../netsurf.zig"); const iterator = @import("../iterator/iterator.zig"); const SessionState = @import("../env.zig").SessionState; +const log = std.log.scoped(.form_data); + pub const Interfaces = .{ FormData, KeyIterable, @@ -31,7 +34,7 @@ pub const Interfaces = .{ }; // We store the values in an ArrayList rather than a an -// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(), +// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(), // values() and entries()) work. The FormData can contain duplicate keys, and // each iteration yields 1 key=>value pair. So, given: // @@ -51,10 +54,20 @@ pub const Interfaces = .{ pub const FormData = struct { entries: std.ArrayListUnmanaged(Entry), - pub fn constructor() FormData { - return .{ - .entries = .empty, - }; + pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !FormData { + const form = form_ orelse return .{ .entries = .empty }; + return fromForm(form, submitter_, state, .{}); + } + + const FromFormOpts = struct { + // Uses the state.arena if null. This is needed for when we're handling + // form submission from the Page, and we want to capture the form within + // the session's transfer_arena. + allocator: ?Allocator = null, + }; + pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState, opts: FromFormOpts) !FormData { + const entries = try collectForm(opts.allocator orelse state.arena, form, submitter_, state); + return .{ .entries = entries }; } pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 { @@ -186,9 +199,167 @@ const EntryIterator = struct { } }; +fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.ElementHTML, state: *SessionState) !std.ArrayListUnmanaged(Entry) { + const collection = try parser.formGetCollection(form); + const len = try parser.htmlCollectionGetLength(collection); + + var entries: std.ArrayListUnmanaged(Entry) = .empty; + try entries.ensureTotalCapacity(arena, len); + + const submitter_name_ = try getSubmitterName(submitter_); + + for (0..len) |i| { + const node = try parser.htmlCollectionItem(collection, @intCast(i)); + const element = parser.nodeToElement(node); + + // must have a name + const name = try parser.elementGetAttribute(element, "name") orelse continue; + if (try parser.elementGetAttribute(element, "disabled") != null) { + continue; + } + + const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element))); + switch (tag) { + .input => { + const input_type = try parser.elementGetAttribute(element, "type") orelse ""; + if (std.ascii.eqlIgnoreCase(input_type, "image")) { + if (submitter_name_) |submitter_name| { + if (std.mem.eql(u8, submitter_name, name)) { + try entries.append(arena, .{ + .key = try std.fmt.allocPrint(arena, "{s}.x", .{name}), + .value = "0", + }); + try entries.append(arena, .{ + .key = try std.fmt.allocPrint(arena, "{s}.y", .{name}), + .value = "0", + }); + } + } + continue; + } + if (std.ascii.eqlIgnoreCase(input_type, "checkbox") or std.ascii.eqlIgnoreCase(input_type, "radio")) { + if (try parser.inputGetChecked(@ptrCast(element)) == false) { + continue; + } + } + const value = (try parser.elementGetAttribute(element, "value")) orelse ""; + try entries.append(arena, .{ .key = name, .value = value }); + }, + .select => { + const select: *parser.Select = @ptrCast(node); + try collectSelectValues(arena, select, name, &entries, state); + }, + .textarea => { + const textarea: *parser.TextArea = @ptrCast(node); + const value = try parser.textareaGetValue(textarea); + try entries.append(arena, .{ .key = name, .value = value }); + }, + .button => if (submitter_name_) |submitter_name| { + if (std.mem.eql(u8, submitter_name, name)) { + const value = (try parser.elementGetAttribute(element, "value")) orelse ""; + try entries.append(arena, .{ .key = name, .value = value }); + } + }, + else => { + log.warn("unsupported form element: {s}\n", .{@tagName(tag)}); + continue; + }, + } + } + + return entries; +} + +fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *std.ArrayListUnmanaged(Entry), state: *SessionState) !void { + const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement; + + // Go through the HTMLSelectElement because it has specific logic for handling + // the default selected option, which libdom doesn't properly handle + const selected_index = try HTMLSelectElement.get_selectedIndex(select, state); + if (selected_index == -1) { + return; + } + std.debug.assert(selected_index >= 0); + + const options = try parser.selectGetOptions(select); + const is_multiple = try parser.selectGetMultiple(select); + if (is_multiple == false) { + const option = try parser.optionCollectionItem(options, @intCast(selected_index)); + const value = try parser.optionGetValue(option); + return entries.append(arena, .{ .key = name, .value = value }); + } + + const len = try parser.optionCollectionGetLength(options); + + // we can go directly to the first one + for (@intCast(selected_index)..len) |i| { + const option = try parser.optionCollectionItem(options, @intCast(i)); + if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) { + continue; + } + + if (try parser.optionGetSelected(option)) { + const value = try parser.optionGetValue(option); + try entries.append(arena, .{ .key = name, .value = value }); + } + } +} + +fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 { + const submitter = submitter_ orelse return null; + + const tag = try parser.elementHTMLGetTagType(submitter); + const element: *parser.Element = @ptrCast(submitter); + const name = try parser.elementGetAttribute(element, "name"); + + switch (tag) { + .button => return name, + .input => { + const tpe = (try parser.elementGetAttribute(element, "type")) orelse ""; + // only an image type can be a sumbitter + if (std.ascii.eqlIgnoreCase(tpe, "image")) { + return name; + } + }, + else => {}, + } + return error.InvalidArgument; +} + const testing = @import("../../testing.zig"); -test "FormData" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); +test "Browser.FormData" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = + \\
+ \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\
+ }); defer runner.deinit(); try runner.testCases(&.{ @@ -244,4 +415,28 @@ test "FormData" { .{ "acc = [];", null }, .{ "for (const entry of f) { acc.push(entry) }; acc;", "b,3" }, }, .{}); + + try runner.testCases(&.{ + .{ "let f2 = new FormData(document.getElementById('form1'))", null }, + .{ "acc = '';", null }, + .{ + \\ for (const entry of f2) { + \\ acc += entry[0] + '=' + entry[1] + '\n'; + \\ }; + \\ acc.slice(0, -1) + , + \\txt-1=txt-1-v + \\txt-2=txt-2-v + \\chk-3=chk-3-vb + \\chk-3=chk-3-vc + \\rdi-1=rdi-1-vc + \\ta-1= ta-1-v + \\ta= + \\h1=h1-v + \\sel-1=blue + \\sel-2=sel-2-v + \\mlt-2=water + \\mlt-2=tea + }, + }, .{}); }