From 4586fb1d13e8594beacf0b760f5087931fa056b2 Mon Sep 17 00:00:00 2001 From: egrs Date: Mon, 9 Mar 2026 08:23:19 +0100 Subject: [PATCH 1/3] add Range.getBoundingClientRect and getClientRects headless stubs returning zero-valued DOMRect / empty list per CSSOM View spec. fixes "getBoundingClientRect is not a function" errors on sites where layout code calls this on Range objects (e.g. airbnb). --- src/browser/tests/range.html | 20 ++++++++++++++++++++ src/browser/webapi/Range.zig | 12 ++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/browser/tests/range.html b/src/browser/tests/range.html index 5e9afbdc..2a25e272 100644 --- a/src/browser/tests/range.html +++ b/src/browser/tests/range.html @@ -1022,3 +1022,23 @@ testing.expectEqual('Stnd', div.textContent); } + + + + diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 21a3ce12..6a785cde 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -25,6 +25,7 @@ const Page = @import("../Page.zig"); const Node = @import("Node.zig"); const DocumentFragment = @import("DocumentFragment.zig"); const AbstractRange = @import("AbstractRange.zig"); +const DOMRect = @import("DOMRect.zig"); const Range = @This(); @@ -643,6 +644,15 @@ fn nextAfterSubtree(node: *Node, root: *Node) ?*Node { return null; } +// Headless browser has no layout — return a zero-valued DOMRect. +pub fn getBoundingClientRect(_: *const Range) DOMRect { + return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; +} + +pub fn getClientRects(_: *const Range) []DOMRect { + return &.{}; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Range); @@ -681,6 +691,8 @@ pub const JsApi = struct { pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true }); pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true }); pub const toString = bridge.function(Range.toString, .{ .dom_exception = true }); + pub const getBoundingClientRect = bridge.function(Range.getBoundingClientRect, .{}); + pub const getClientRects = bridge.function(Range.getClientRects, .{}); }; const testing = @import("../../testing.zig"); From e239f69f6974594046f4e94728a116be0005a3a5 Mon Sep 17 00:00:00 2001 From: egrs Date: Mon, 9 Mar 2026 10:09:11 +0100 Subject: [PATCH 2/3] delegate Range rect methods to container element Instead of always returning zeros, delegate getBoundingClientRect and getClientRects to the common ancestor container element. Return zeros only when the range is collapsed or has no element ancestor. --- src/browser/tests/range.html | 31 +++++++++++++++++++++++++++++-- src/browser/webapi/Range.zig | 28 +++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/browser/tests/range.html b/src/browser/tests/range.html index 2a25e272..d9a8637b 100644 --- a/src/browser/tests/range.html +++ b/src/browser/tests/range.html @@ -1023,7 +1023,7 @@ } - - + + + + diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 6a785cde..f0c904a6 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -644,13 +644,31 @@ fn nextAfterSubtree(node: *Node, root: *Node) ?*Node { return null; } -// Headless browser has no layout — return a zero-valued DOMRect. -pub fn getBoundingClientRect(_: *const Range) DOMRect { - return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; +pub fn getBoundingClientRect(self: *const Range, page: *Page) DOMRect { + if (self._proto.getCollapsed()) { + return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; + } + const element = self.getContainerElement() orelse { + return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 }; + }; + return element.getBoundingClientRect(page); } -pub fn getClientRects(_: *const Range) []DOMRect { - return &.{}; +pub fn getClientRects(self: *const Range, page: *Page) ![]DOMRect { + if (self._proto.getCollapsed()) { + return &.{}; + } + const element = self.getContainerElement() orelse { + return &.{}; + }; + return element.getClientRects(page); +} + +fn getContainerElement(self: *const Range) ?*Node.Element { + const container = self._proto.getCommonAncestorContainer(); + if (container.is(Node.Element)) |el| return el; + const parent = container.parentNode() orelse return null; + return parent.is(Node.Element); } pub const JsApi = struct { From 3e5f6023960a26685ebdd1d7d89b974f4322e17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Mon, 9 Mar 2026 18:25:09 +0900 Subject: [PATCH 3/3] zig fmt --- src/browser/webapi/css/FontFace.zig | 2 +- src/browser/webapi/css/FontFaceSet.zig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/webapi/css/FontFace.zig b/src/browser/webapi/css/FontFace.zig index 0da560fc..f824259a 100644 --- a/src/browser/webapi/css/FontFace.zig +++ b/src/browser/webapi/css/FontFace.zig @@ -30,7 +30,7 @@ _family: []const u8, pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace { _ = source; - const arena = try page.getArena(.{.debug = "FontFace"}); + const arena = try page.getArena(.{ .debug = "FontFace" }); errdefer page.releaseArena(arena); const self = try arena.create(FontFace); diff --git a/src/browser/webapi/css/FontFaceSet.zig b/src/browser/webapi/css/FontFaceSet.zig index 12f090bd..6e5cd941 100644 --- a/src/browser/webapi/css/FontFaceSet.zig +++ b/src/browser/webapi/css/FontFaceSet.zig @@ -28,7 +28,7 @@ const FontFaceSet = @This(); _arena: Allocator, pub fn init(page: *Page) !*FontFaceSet { - const arena = try page.getArena(.{.debug = "FontFaceSet"}); + const arena = try page.getArena(.{ .debug = "FontFaceSet" }); errdefer page.releaseArena(arena); const self = try arena.create(FontFaceSet); @@ -73,7 +73,7 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; pub const weak = true; - pub const finalizer = bridge.finalizer(FontFaceSet.deinit); + pub const finalizer = bridge.finalizer(FontFaceSet.deinit); }; pub const size = bridge.property(0, .{ .template = false, .readonly = true });