From 46f1646cf054038cb671ae240a61bdbd210e1870 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Tue, 28 Apr 2026 00:12:22 +0200 Subject: [PATCH 1/3] forms: honor formaction / formmethod / formenctype on submit button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a form is submitted via a click on a submit button (or form.requestSubmit(button)), the HTML form-submission algorithm requires the submitter's formaction / formmethod / formenctype attributes to override the form's corresponding attributes when present. Frame.submitForm was reading action / method / enctype only from the form element, so the button-side overrides were silently ignored. The symmetrical lookup for formtarget already exists upstream — this commit applies the same pattern to the other three attributes. Closes #2278 --- src/browser/Frame.zig | 25 ++++++++-- src/browser/tests/frames/target.html | 70 ++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 07dadac3..9ba0be76 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -3735,7 +3735,16 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For const arena = try self._session.getArena(.medium, "submitForm"); errdefer self._session.releaseArena(arena); - const enctype = form_element.getAttributeSafe(comptime .wrap("enctype")); + // Per HTML spec form-submission algorithm, when the submitter is a submit + // button, its formaction/formmethod/formenctype attributes override the + // form's corresponding attributes (matching how formtarget is honored above). + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit + const enctype = blk: { + if (submitter_) |s| { + if (s.getAttributeSafe(comptime .wrap("formenctype"))) |fe| break :blk fe; + } + break :blk form_element.getAttributeSafe(comptime .wrap("enctype")); + }; // Get charset from accept-charset attribute or fall back to document charset const charset: []const u8 = blk: { @@ -3752,8 +3761,18 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For var buf = std.Io.Writer.Allocating.init(arena); try form_data.write(.{ .enctype = enctype, .charset = charset, .allocator = arena }, &buf.writer); - const method = form_element.getAttributeSafe(comptime .wrap("method")) orelse ""; - var action = form_element.getAttributeSafe(comptime .wrap("action")) orelse self.url; + const method = blk: { + if (submitter_) |s| { + if (s.getAttributeSafe(comptime .wrap("formmethod"))) |fm| break :blk fm; + } + break :blk form_element.getAttributeSafe(comptime .wrap("method")) orelse ""; + }; + var action = blk: { + if (submitter_) |s| { + if (s.getAttributeSafe(comptime .wrap("formaction"))) |fa| break :blk fa; + } + break :blk form_element.getAttributeSafe(comptime .wrap("action")) orelse self.url; + }; var opts = NavigateOpts{ .reason = .form, diff --git a/src/browser/tests/frames/target.html b/src/browser/tests/frames/target.html index dbf23242..924e0e59 100644 --- a/src/browser/tests/frames/target.html +++ b/src/browser/tests/frames/target.html @@ -56,3 +56,73 @@ }); } + + + +
+ +
+ + + + + +
+ + +
+ + + + + +
+ + +
+ + From f3ab1dcf78124345db1e6affbfa9053931ec9439 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 27 Apr 2026 17:17:50 +0800 Subject: [PATCH 2/3] Add constructor arity Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/174 Builds ontop of https://github.com/lightpanda-io/browser/pull/2272 largely because it's driven by the same uievents WPT tests. https://github.com/lightpanda-io/browser/pull/1498 added the length to functions, and this does it for constructors. --- src/browser/js/Snapshot.zig | 7 ++- src/browser/js/bridge.zig | 56 +++++++++++++------ .../tests/element/html/script/script.html | 6 ++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 533d479c..93bf9f6b 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -569,7 +569,12 @@ pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8 break :blk illegalConstructorCallback; }; - const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?; + const arity: c_int = if (@hasDecl(JsApi, "constructor")) JsApi.constructor.arity else 0; + const template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .length = arity, + .callback = callback, + .behavior = v8.kConstructorBehavior_Allow, + }).?; { const internal_field_count = comptime countInternalFields(JsApi); if (internal_field_count > 0) { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 73ddccab..ebe96a1f 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -107,6 +107,7 @@ pub fn Builder(comptime T: type) type { } pub const Constructor = struct { + arity: c_int, func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, const Opts = struct { @@ -118,21 +119,24 @@ pub const Constructor = struct { }; fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor { - return .{ .func = struct { - fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { - const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller: Caller = undefined; - if (!caller.init(v8_isolate)) { - return; - } - defer caller.deinit(); + return .{ + .arity = comptime Function.getArity(@TypeOf(func), if (opts.new_target) 1 else 0), + .func = struct { + fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { + const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; + var caller: Caller = undefined; + if (!caller.init(v8_isolate)) { + return; + } + defer caller.deinit(); - caller.constructor(T, func, handle.?, .{ - .dom_exception = opts.dom_exception, - .new_target = opts.new_target, - }); - } - }.wrap }; + caller.constructor(T, func, handle.?, .{ + .dom_exception = opts.dom_exception, + .new_target = opts.new_target, + }); + } + }.wrap, + }; } }; @@ -149,7 +153,7 @@ pub const Function = struct { .cache = opts.cache, .static = opts.static, .wpt_only = opts.wpt_only, - .arity = getArity(@TypeOf(func)), + .arity = getArity(@TypeOf(func), 1), .func = if (opts.noop) noopFunction else struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { Caller.Function.call(T, handle.?, func, opts); @@ -160,14 +164,32 @@ pub const Function = struct { pub fn noopFunction(_: ?*const v8.FunctionCallbackInfo) callconv(.c) void {} - fn getArity(comptime T: type) usize { + fn getArity(comptime T: type, comptime start: usize) usize { + const Execution = js.Execution; + + const Page = @import("../Page.zig"); + const Session = @import("../Session.zig"); + var count: usize = 0; var params = @typeInfo(T).@"fn".params; - for (params[1..]) |p| { // start at 1, skip self + for (params[start..]) |p| { // start at 1, skip self const PT = p.type.?; if (PT == *Frame or PT == *const Frame) { break; } + + if (PT == *Page or PT == *const Page) { + break; + } + + if (PT == *Execution or PT == *const Execution) { + break; + } + + if (PT == *Session or PT == *const Session) { + break; + } + if (@typeInfo(PT) == .optional) { break; } diff --git a/src/browser/tests/element/html/script/script.html b/src/browser/tests/element/html/script/script.html index d49d025a..0b7fc0f8 100644 --- a/src/browser/tests/element/html/script/script.html +++ b/src/browser/tests/element/html/script/script.html @@ -35,5 +35,11 @@ const append = Object.getOwnPropertyDescriptor(Element.prototype, 'append'); testing.expectEqual('append', append.value.name); + + // Constructor.length should equal the number of required arguments (1 for + // events: the type). Sentinel for the New__Config wiring in Snapshot.zig. + testing.expectEqual(1, MouseEvent.length); + testing.expectEqual(1, KeyboardEvent.length); + testing.expectEqual(1, Event.length); } From 2f6ce92db24efa057c26a32c0eaae584c2f2b54e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 28 Apr 2026 08:56:22 +0800 Subject: [PATCH 3/3] update v8 dep --- .github/actions/install/action.yml | 2 +- Dockerfile | 2 +- build.zig.zon | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index e9ccde0b..727eb4d4 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.4.1' + default: 'v0.4.2' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 33501041..75c3e3fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.4.1 +ARG ZIG_V8=v0.4.2 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index 16bd2378..ce289daa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.1.tar.gz", - .hash = "v8-0.0.0-xddH672HBAA1hQIa2Uv4mzs_qHC9-Py-M5ssqSSVhWtK", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.2.tar.gz", + .hash = "v8-0.0.0-xddH672HBABNrbtyNk9o4QXxQJTlpjiCscmdEQuMvKnR", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{