Merge pull request #669 from lightpanda-io/form_data_from_form

FormData constructor form & submitter parameter
This commit is contained in:
Karl Seguin
2025-05-21 23:36:12 +08:00
committed by GitHub
8 changed files with 617 additions and 34 deletions

View File

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

View File

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

View File

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

54
src/browser/html/form.zig Normal file
View File

@@ -0,0 +1,54 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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

158
src/browser/html/select.zig Normal file
View File

@@ -0,0 +1,158 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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 =
\\ <form id=f1>
\\ <select id=s1 name=s1><option>o1<option>o2</select>
\\ </form>
\\ <select id=s2></select>
});
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" },
}, .{});
}

View File

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

View File

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

View File

@@ -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,181 @@ 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 tpe = try parser.elementGetAttribute(element, "type") orelse "";
if (std.ascii.eqlIgnoreCase(tpe, "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(tpe, "checkbox") or std.ascii.eqlIgnoreCase(tpe, "radio")) {
if (try parser.inputGetChecked(@ptrCast(element)) == false) {
continue;
}
}
if (std.ascii.eqlIgnoreCase(tpe, "submit")) {
if (submitter_name_ == null or !std.mem.eql(u8, submitter_name_.?, name)) {
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));
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
return;
}
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") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
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 =
\\ <form id="form1">
\\ <input id="has_no_name" value="nope1">
\\ <input id="is_disabled" disabled value="nope2">
\\
\\ <input name="txt-1" value="txt-1-v">
\\ <input name="txt-2" value="txt-2-v" type=password>
\\
\\ <input name="chk-3" value="chk-3-va" type=checkbox>
\\ <input name="chk-3" value="chk-3-vb" type=checkbox checked>
\\ <input name="chk-3" value="chk-3-vc" type=checkbox checked>
\\ <input name="chk-4" value="chk-4-va" type=checkbox>
\\ <input name="chk-4" value="chk-4-va" type=checkbox>
\\
\\ <input name="rdi-1" value="rdi-1-va" type=radio>
\\ <input name="rdi-1" value="rdi-1-vb" type=radio>
\\ <input name="rdi-1" value="rdi-1-vc" type=radio checked>
\\ <input name="rdi-2" value="rdi-2-va" type=radio>
\\ <input name="rdi-2" value="rdi-2-vb" type=radio>
\\
\\ <textarea name="ta-1"> ta-1-v</textarea>
\\ <textarea name="ta"></textarea>
\\
\\ <input type=hidden name=h1 value="h1-v">
\\ <input type=hidden name=h2 value="h2-v" disabled=disabled>
\\
\\ <select name="sel-1"><option>blue<option>red</select>
\\ <select name="sel-2"><option>blue<option value=sel-2-v selected>red</select>
\\ <select name="sel-3"><option disabled>nope1<option>nope2</select>
\\ <select name="mlt-1" multiple><option>water<option>tea</select>
\\ <select name="mlt-2" multiple><option selected>water<option selected>tea<option>coffee</select>
\\ <input type=submit id=s1 name=s1 value=s1-v>
\\ <input type=submit name=s2 value=s2-v>
\\ <input type=image name=i1 value=i1-v>
\\ </form>
});
defer runner.deinit();
try runner.testCases(&.{
@@ -244,4 +429,31 @@ test "FormData" {
.{ "acc = [];", null },
.{ "for (const entry of f) { acc.push(entry) }; acc;", "b,3" },
}, .{});
try runner.testCases(&.{
.{ "let form1 = document.getElementById('form1')", null },
.{ "let submit1 = document.getElementById('s1')", null },
.{ "let f2 = new FormData(form1, submit1)", 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
\\s1=s1-v
},
}, .{});
}