From 54069e565cf7ff451d88ab48377c0ab9a03e73ae Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 16:15:27 +0200 Subject: [PATCH] webapi: NodeFilter follows WebIDL callback interface semantics Fixes the 5 failing subtests of WPT /dom/traversal/TreeWalker-acceptNode-filter.html (7/12 -> 12/12): - A filter object was converted eagerly at createTreeWalker time, so an object without a callable acceptNode threw "invalid argument" at creation. Per WebIDL any object converts to the NodeFilter callback interface; the TypeError belongs at invocation time. - The acceptNode member was cached at conversion. Per "call a user object's operation" it must be looked up with a fresh Get on every traversal, rethrowing errors from a throwing getter. - The callback was invoked with the default this; the spec requires the filter object itself as the this value. NodeFilter now stores the raw function or object (js.Object.Global) and performs the per-invocation lookup, callability check and this-binding in acceptNode. Coverage: /dom/traversal/TreeWalker-acceptNode-filter.html 7/12 -> 12/12 (fully green). Co-Authored-By: Claude Fable 5 --- src/browser/webapi/DOMNodeIterator.zig | 2 +- src/browser/webapi/DOMTreeWalker.zig | 2 +- src/browser/webapi/NodeFilter.zig | 39 +++++++++++++++----------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/browser/webapi/DOMNodeIterator.zig b/src/browser/webapi/DOMNodeIterator.zig index 6756e4d75..8d349265e 100644 --- a/src/browser/webapi/DOMNodeIterator.zig +++ b/src/browser/webapi/DOMNodeIterator.zig @@ -60,7 +60,7 @@ pub fn getWhatToShow(self: *const DOMNodeIterator) u32 { } pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts { - return self._filter._original_filter; + return self._filter._opts; } pub fn nextNode(self: *DOMNodeIterator, frame: *Frame) !?*Node { diff --git a/src/browser/webapi/DOMTreeWalker.zig b/src/browser/webapi/DOMTreeWalker.zig index 483b94a98..354c3c2e2 100644 --- a/src/browser/webapi/DOMTreeWalker.zig +++ b/src/browser/webapi/DOMTreeWalker.zig @@ -52,7 +52,7 @@ pub fn getWhatToShow(self: *const DOMTreeWalker) u32 { } pub fn getFilter(self: *const DOMTreeWalker) ?FilterOpts { - return self._filter._original_filter; + return self._filter._opts; } pub fn getCurrentNode(self: *const DOMTreeWalker) *Node { diff --git a/src/browser/webapi/NodeFilter.zig b/src/browser/webapi/NodeFilter.zig index a2519e718..d64aa4d26 100644 --- a/src/browser/webapi/NodeFilter.zig +++ b/src/browser/webapi/NodeFilter.zig @@ -21,27 +21,17 @@ const Node = @import("Node.zig"); const NodeFilter = @This(); -_func: ?js.Function.Global, -_original_filter: ?FilterOpts, +_opts: ?FilterOpts, pub const FilterOpts = union(enum) { function: js.Function.Global, - object: struct { - pub const js_as_object = true; - acceptNode: js.Function.Global, - }, + // Any object is a valid callback interface; whether its acceptNode + // member is callable is only checked when the filter is invoked. + object: js.Object.Global, }; pub fn init(opts_: ?FilterOpts) !NodeFilter { - const opts = opts_ orelse return .{ ._func = null, ._original_filter = null }; - const func = switch (opts) { - .function => |func| func, - .object => |obj| obj.acceptNode, - }; - return .{ - ._func = func, - ._original_filter = opts_, - }; + return .{ ._opts = opts_ }; } // Constants @@ -65,8 +55,23 @@ pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400; pub const SHOW_NOTATION: u32 = 0x800; pub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 { - const func = self._func orelse return FILTER_ACCEPT; - return local.toLocal(func).callRethrow(i32, .{node}); + const opts = self._opts orelse return FILTER_ACCEPT; + switch (opts) { + .function => |func| return local.toLocal(func).callRethrow(i32, .{node}), + .object => |obj| { + // Per WebIDL "call a user object's operation": the acceptNode + // member is looked up on every invocation (rethrowing getter + // errors), must be callable (TypeError otherwise), and is + // invoked with the filter object as its this value. + const filter_obj = obj.local(local); + const member = try filter_obj.get("acceptNode"); + if (!member.isFunction()) { + return error.TypeError; + } + const func = js.Function{ .local = local, .handle = @ptrCast(member.handle) }; + return func.callWithThisRethrow(i32, filter_obj, .{node}); + }, + } } pub fn shouldShow(node: *const Node, what_to_show: u32) bool {