From 9441e4fec8b51fc5bf1b97f269e7c5f673b220e2 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 27 Apr 2026 15:57:11 +0800 Subject: [PATCH] Improve WPT uievents/ tests 1. Add a bunch of initXYZEvent functions, and fix the overload of initTextEvent 2. Add a number of missing (mostly legacy) accessors, e.g. uievent.getWhich() 3. CompositionEvent inherits from UIEvent, not Event 4. Give every accessor get/set a name. E.g the "name" of script.src is "get script" and "set script". --- src/browser/js/Snapshot.zig | 29 ++++++--- .../tests/element/html/script/script.html | 10 +++ src/browser/tests/event/composition.html | 36 +++++++++++ src/browser/tests/event/keyboard.html | 51 +++++++++++++++ src/browser/tests/event/mouse.html | 64 +++++++++++++++++++ src/browser/tests/event/text.html | 35 +++++++--- src/browser/tests/event/ui.html | 30 +++++++++ src/browser/webapi/Document.zig | 5 ++ src/browser/webapi/Event.zig | 2 - src/browser/webapi/event/CompositionEvent.zig | 31 ++++++++- src/browser/webapi/event/KeyboardEvent.zig | 51 +++++++++++++++ src/browser/webapi/event/MouseEvent.zig | 63 ++++++++++++++++++ src/browser/webapi/event/TextEvent.zig | 19 +++--- src/browser/webapi/event/UIEvent.zig | 33 +++++++++- 14 files changed, 425 insertions(+), 34 deletions(-) diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index c625127c..533d479c 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -276,9 +276,11 @@ fn createSnapshotContext( const name = JsApi.Meta.name; const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); var maybe_result: v8.MaybeBool = undefined; - var properties: v8.PropertyAttribute = v8.None; - if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) { - properties |= v8.DontEnum; + // Web IDL: interface objects on the global are non-enumerable + // by default. Opt back in via JsApi.Meta.enumerable = true. + var properties: v8.PropertyAttribute = v8.DontEnum; + if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == true) { + properties = v8.None; } v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result); } @@ -578,6 +580,8 @@ pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8 const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi); const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len)); v8.v8__FunctionTemplate__SetClassName(template, class_name); + // Web IDL: interface object's `prototype` property is non-writable/non-configurable. + v8.v8__FunctionTemplate__ReadOnlyPrototype(template); return template; } @@ -615,13 +619,21 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F .callback = value.getter, .signature = getter_signature, }).?; - const setter_callback = if (value.setter) |setter| - v8.v8__FunctionTemplate__New__Config(isolate, &.{ + // WebIDL: getter function's .name should be "get X" + const getter_name_str = "get " ++ name; + const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len)); + v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8); + + const setter_callback = if (value.setter) |setter| blk: { + const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = setter, .signature = getter_signature, - }).? - else - null; + }).?; + const setter_name_str = "set " ++ name; + const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len)); + v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8); + break :blk cb; + } else null; var attribute: v8.PropertyAttribute = 0; if (value.setter == null) { @@ -655,6 +667,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F .signature = func_signature, }).?; const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); + v8.v8__FunctionTemplate__SetClassName(function_template, js_name); if (value.static) { v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None); } else { diff --git a/src/browser/tests/element/html/script/script.html b/src/browser/tests/element/html/script/script.html index 4f826fe7..d49d025a 100644 --- a/src/browser/tests/element/html/script/script.html +++ b/src/browser/tests/element/html/script/script.html @@ -25,5 +25,15 @@ testing.expectEqual(true, dom_load); testing.expectEqual(true, attribute_load); }); + + // Web IDL requires attribute getters/setters to be named "get X" / "set X" + // and methods to use their identifier. Sentinel for the binding-level naming + // fix in Snapshot.zig — Script.src exercises both accessor paths. + const desc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src'); + testing.expectEqual('get src', desc.get.name); + testing.expectEqual('set src', desc.set.name); + + const append = Object.getOwnPropertyDescriptor(Element.prototype, 'append'); + testing.expectEqual('append', append.value.name); } diff --git a/src/browser/tests/event/composition.html b/src/browser/tests/event/composition.html index b5a6a710..e9602335 100644 --- a/src/browser/tests/event/composition.html +++ b/src/browser/tests/event/composition.html @@ -5,10 +5,13 @@ { let event = new CompositionEvent("test", {}); testing.expectEqual(true, event instanceof CompositionEvent); + testing.expectEqual(true, event instanceof UIEvent); testing.expectEqual(true, event instanceof Event); testing.expectEqual("test", event.type); testing.expectEqual("", event.data); + testing.expectEqual(0, event.detail); + testing.expectEqual(window, event.view); } @@ -34,3 +37,36 @@ } + + + + + + diff --git a/src/browser/tests/event/keyboard.html b/src/browser/tests/event/keyboard.html index 79baff7e..223a9e0b 100644 --- a/src/browser/tests/event/keyboard.html +++ b/src/browser/tests/event/keyboard.html @@ -116,3 +116,54 @@ testing.expectEqual(true, called); } + + + + + + + + diff --git a/src/browser/tests/event/mouse.html b/src/browser/tests/event/mouse.html index 83fc4045..5314889b 100644 --- a/src/browser/tests/event/mouse.html +++ b/src/browser/tests/event/mouse.html @@ -52,3 +52,67 @@ document.dispatchEvent(new MouseEvent('mousetest')); testing.expectEqual(false, mouseIsTrusted); + + + + + + + + + + diff --git a/src/browser/tests/event/text.html b/src/browser/tests/event/text.html index f885c293..61894502 100644 --- a/src/browser/tests/event/text.html +++ b/src/browser/tests/event/text.html @@ -1,17 +1,32 @@ + + diff --git a/src/browser/tests/event/ui.html b/src/browser/tests/event/ui.html index 50d880ec..f060622a 100644 --- a/src/browser/tests/event/ui.html +++ b/src/browser/tests/event/ui.html @@ -52,3 +52,33 @@ const evt7 = new UIEvent('custom', { bubbles: true }); testing.expectEqual(0, evt7.detail); + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index a00d301b..a30d32cd 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -396,6 +396,11 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !* return (try TextEvent.init("", null, frame)).asEvent(); } + if (std.mem.eql(u8, normalized, "compositionevent")) { + const CompositionEvent = @import("event/CompositionEvent.zig"); + return (try CompositionEvent.init("", null, frame)).asEvent(); + } + return error.NotSupported; } diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index f08914e8..01a3617a 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -71,7 +71,6 @@ pub const Type = union(enum) { custom_event: *@import("event/CustomEvent.zig"), message_event: *@import("event/MessageEvent.zig"), progress_event: *@import("event/ProgressEvent.zig"), - composition_event: *@import("event/CompositionEvent.zig"), navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"), page_transition_event: *@import("event/PageTransitionEvent.zig"), pop_state_event: *@import("event/PopStateEvent.zig"), @@ -163,7 +162,6 @@ pub fn is(self: *Event, comptime T: type) ?*T { .custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null, .message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null, .progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null, - .composition_event => |e| return if (T == @import("event/CompositionEvent.zig")) e else null, .navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null, .page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null, .pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null, diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index b0492a92..3d25a156 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -22,12 +22,14 @@ const js = @import("../../js/js.zig"); const Frame = @import("../../Frame.zig"); const Event = @import("../Event.zig"); +const UIEvent = @import("UIEvent.zig"); +const Window = @import("../Window.zig"); const String = lp.String; const CompositionEvent = @This(); -_proto: *Event, +_proto: *UIEvent, _data: []const u8 = "", const CompositionEventOptions = struct { @@ -42,7 +44,7 @@ pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CompositionEvent const type_string = try String.init(arena, typ, .{}); const opts = opts_ orelse Options{}; - const event = try frame._factory.event( + const event = try frame._factory.uiEvent( arena, type_string, CompositionEvent{ @@ -56,13 +58,35 @@ pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CompositionEvent } pub fn asEvent(self: *CompositionEvent) *Event { - return self._proto; + return self._proto.asEvent(); } pub fn getData(self: *const CompositionEvent) []const u8 { return self._data; } +pub fn initCompositionEvent( + self: *CompositionEvent, + typ: []const u8, + bubbles: ?bool, + cancelable: ?bool, + view: ?*Window, + data: ?[]const u8, +) !void { + const ui = self._proto; + const event = ui._proto; + if (event._event_phase != .none) { + return; + } + + const arena = event._arena; + event._type_string = try String.init(arena, typ, .{}); + event._bubbles = bubbles orelse false; + event._cancelable = cancelable orelse false; + ui._view = view; + self._data = if (data) |d| try arena.dupe(u8, d) else ""; +} + pub const JsApi = struct { pub const bridge = js.Bridge(CompositionEvent); @@ -74,6 +98,7 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(CompositionEvent.init, .{}); pub const data = bridge.accessor(CompositionEvent.getData, null, .{}); + pub const initCompositionEvent = bridge.function(CompositionEvent.initCompositionEvent, .{}); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index b2658d02..979b5c29 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -270,6 +270,49 @@ pub fn getShiftKey(self: *const KeyboardEvent) bool { return self._shift_key; } +// Deprecated: tracked as 0 since we don't synthesise legacy character codes. +pub fn getCharCode(self: *const KeyboardEvent) u32 { + _ = self; + return 0; +} + +pub fn getKeyCode(self: *const KeyboardEvent) u32 { + _ = self; + return 0; +} + +pub fn initKeyboardEvent( + self: *KeyboardEvent, + typ: []const u8, + bubbles: ?bool, + cancelable: ?bool, + view: ?*@import("../Window.zig"), + key: ?[]const u8, + location: ?u32, + ctrl_key: ?bool, + alt_key: ?bool, + shift_key: ?bool, + meta_key: ?bool, +) !void { + const ui = self._proto; + const event = ui._proto; + if (event._event_phase != .none) { + return; + } + + const arena = event._arena; + event._type_string = try String.init(arena, typ, .{}); + event._bubbles = bubbles orelse false; + event._cancelable = cancelable orelse false; + ui._view = view; + self._key = try Key.fromString(arena, key orelse ""); + self._location = std.meta.intToEnum(Location, location orelse 0) catch return error.TypeError; + self._ctrl_key = ctrl_key orelse false; + self._alt_key = alt_key orelse false; + self._shift_key = shift_key orelse false; + self._meta_key = meta_key orelse false; +} + pub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool { const key = try Key.fromString(self._proto._proto._arena, str); @@ -309,7 +352,15 @@ pub const JsApi = struct { pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{}); pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{}); pub const shiftKey = bridge.accessor(KeyboardEvent.getShiftKey, null, .{}); + pub const charCode = bridge.accessor(KeyboardEvent.getCharCode, null, .{}); + pub const keyCode = bridge.accessor(KeyboardEvent.getKeyCode, null, .{}); pub const getModifierState = bridge.function(KeyboardEvent.getModifierState, .{}); + pub const initKeyboardEvent = bridge.function(KeyboardEvent.initKeyboardEvent, .{}); + + pub const DOM_KEY_LOCATION_STANDARD = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_STANDARD), .{ .template = true }); + pub const DOM_KEY_LOCATION_LEFT = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_LEFT), .{ .template = true }); + pub const DOM_KEY_LOCATION_RIGHT = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_RIGHT), .{ .template = true }); + pub const DOM_KEY_LOCATION_NUMPAD = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_NUMPAD), .{ .template = true }); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index eaa0315e..240ce6c8 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -193,6 +193,65 @@ pub fn getShiftKey(self: *const MouseEvent) bool { return self._shift_key; } +// Deprecated: tracks the same value as offsetX/clientX in the absence of layout. +pub fn getLayerX(self: *const MouseEvent) f64 { + return self._client_x; +} + +pub fn getLayerY(self: *const MouseEvent) f64 { + return self._client_y; +} + +pub fn getModifierState(self: *const MouseEvent, key: []const u8) bool { + if (std.mem.eql(u8, key, "Alt") or std.mem.eql(u8, key, "AltGraph")) return self._alt_key; + if (std.mem.eql(u8, key, "Control")) return self._ctrl_key; + if (std.mem.eql(u8, key, "Shift")) return self._shift_key; + if (std.mem.eql(u8, key, "Meta")) return self._meta_key; + if (std.mem.eql(u8, key, "Accel")) return self._ctrl_key or self._meta_key; + return false; +} + +pub fn initMouseEvent( + self: *MouseEvent, + typ: []const u8, + bubbles: ?bool, + cancelable: ?bool, + view: ?*@import("../Window.zig"), + detail: ?i32, + screen_x: ?i32, + screen_y: ?i32, + client_x: ?i32, + client_y: ?i32, + ctrl_key: ?bool, + alt_key: ?bool, + shift_key: ?bool, + meta_key: ?bool, + button: ?i16, + related_target: ?*EventTarget, +) !void { + const ui = self._proto; + const event = ui._proto; + if (event._event_phase != .none) { + return; + } + + event._type_string = try String.init(event._arena, typ, .{}); + event._bubbles = bubbles orelse false; + event._cancelable = cancelable orelse false; + ui._view = view; + ui._detail = if (detail) |d| @intCast(@max(d, 0)) else 0; + self._screen_x = @floatFromInt(screen_x orelse 0); + self._screen_y = @floatFromInt(screen_y orelse 0); + self._client_x = @floatFromInt(client_x orelse 0); + self._client_y = @floatFromInt(client_y orelse 0); + self._ctrl_key = ctrl_key orelse false; + self._alt_key = alt_key orelse false; + self._shift_key = shift_key orelse false; + self._meta_key = meta_key orelse false; + self._button = std.meta.intToEnum(MouseButton, button orelse 0) catch return error.TypeError; + self._related_target = related_target; +} + pub const JsApi = struct { pub const bridge = js.Bridge(MouseEvent); @@ -218,8 +277,12 @@ pub const JsApi = struct { pub const screenX = bridge.accessor(getScreenX, null, .{}); pub const screenY = bridge.accessor(getScreenY, null, .{}); pub const shiftKey = bridge.accessor(getShiftKey, null, .{}); + pub const layerX = bridge.accessor(getLayerX, null, .{}); + pub const layerY = bridge.accessor(getLayerY, null, .{}); pub const x = bridge.accessor(getClientX, null, .{}); pub const y = bridge.accessor(getClientY, null, .{}); + pub const getModifierState = bridge.function(MouseEvent.getModifierState, .{}); + pub const initMouseEvent = bridge.function(MouseEvent.initMouseEvent, .{}); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig index 74dc18fc..7662320f 100644 --- a/src/browser/webapi/event/TextEvent.zig +++ b/src/browser/webapi/event/TextEvent.zig @@ -72,24 +72,23 @@ pub fn getData(self: *const TextEvent) []const u8 { pub fn initTextEvent( self: *TextEvent, typ: []const u8, - bubbles: bool, - cancelable: bool, + bubbles: ?bool, + cancelable: ?bool, view: ?*@import("../Window.zig"), - data: []const u8, + data: ?[]const u8, ) !void { - _ = view; // view parameter is ignored in modern implementations - - const event = self._proto._proto; + const ui = self._proto; + const event = ui._proto; if (event._event_phase != .none) { - // Only allow initialization if event hasn't been dispatched return; } const arena = event._arena; event._type_string = try String.init(arena, typ, .{}); - event._bubbles = bubbles; - event._cancelable = cancelable; - self._data = try arena.dupe(u8, data); + event._bubbles = bubbles orelse false; + event._cancelable = cancelable orelse false; + ui._view = view; + self._data = if (data) |d| try arena.dupe(u8, d) else ""; } pub const JsApi = struct { diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 4122edde..13671c5e 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -40,6 +40,7 @@ pub const Type = union(enum) { focus_event: *@import("FocusEvent.zig"), text_event: *@import("TextEvent.zig"), input_event: *@import("InputEvent.zig"), + composition_event: *@import("CompositionEvent.zig"), }; pub const UIEventOptions = struct { @@ -88,6 +89,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T { .focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null, .text_event => |e| return if (T == @import("TextEvent.zig")) e else null, .input_event => |e| return if (T == @import("InputEvent.zig")) e else null, + .composition_event => |e| return if (T == @import("CompositionEvent.zig")) e else null, } return null; } @@ -111,7 +113,34 @@ pub fn getView(self: *UIEvent, frame: *Frame) *Window { return self._view orelse frame.window; } -// deprecated `initUIEvent()` not implemented +// Legacy: see https://w3c.github.io/uievents/#dom-uievent-which +pub fn getWhich(self: *const UIEvent) u32 { + return switch (self._type) { + .mouse_event => |me| @as(u32, @intCast(me.getButton())) + 1, + .keyboard_event => 0, + else => 0, + }; +} + +pub fn initUIEvent( + self: *UIEvent, + typ: []const u8, + bubbles: ?bool, + cancelable: ?bool, + view: ?*Window, + detail: ?i32, +) !void { + const event = self._proto; + if (event._event_phase != .none) { + return; + } + + event._type_string = try String.init(event._arena, typ, .{}); + event._bubbles = bubbles orelse false; + event._cancelable = cancelable orelse false; + self._view = view; + self._detail = if (detail) |d| @intCast(@max(d, 0)) else 0; +} pub const JsApi = struct { pub const bridge = js.Bridge(UIEvent); @@ -125,6 +154,8 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(UIEvent.init, .{}); pub const detail = bridge.accessor(UIEvent.getDetail, null, .{}); pub const view = bridge.accessor(UIEvent.getView, null, .{}); + pub const which = bridge.accessor(UIEvent.getWhich, null, .{}); + pub const initUIEvent = bridge.function(UIEvent.initUIEvent, .{}); }; const testing = @import("../../../testing.zig");