Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions moli-cdp-smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ Offscreen canvases: viewport initialization, setter/conversion errors, copied
Int32Array queries, context isolation, context reacquisition, resize retention,
and clamping to the advertised maximum. It is not a GPU rendering test.

The default `svg-rect` group checks the detached `SVGRect` interface used by
SVG capability detection, sharing the renderer fixture for prototype, identity,
and restricted-float conversion contracts. It was calibrated on 2026-09-07
against Debian `/usr/bin/chromium` 145.0.7632.116 and runs independently of
IndexedDB startup coverage.

Covered well:

- The default raw `debugger-breakpoints`, `runtime-exception`, and
Expand Down
28 changes: 28 additions & 0 deletions moli-cdp-smoke/moli_cdp_smoke/groups/svg_rect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from typing import Any

from ..assertions import assert_equal, record_contract
from ..config import REPO_ROOT


async def run_svg_rect_group(
browser: Any, fixture: str, results: list[dict[str, Any]]
) -> None:
context = await browser.new_context()
try:
page = await context.new_page()
await page.goto(f"{fixture}/plain", wait_until="load", timeout=10_000)
script = (REPO_ROOT / "moli-renderer-v8/tests/fixtures/svg-create-rect.js").read_text()
observed = await page.evaluate(script)
assert_equal(observed, "svg-create-rect:ok", "SVGRect interface and value contract")
record_contract(
results,
"svg_create_rect",
contract="SVG capability detection has a real detached SVGRect with restricted float fields.",
source="Chromium 145.0.7632.116 executable probe (2026-09-07)",
commands=["Runtime.evaluate"],
observed=observed,
)
finally:
await context.close()
7 changes: 7 additions & 0 deletions moli-cdp-smoke/moli_cdp_smoke/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from .groups.proxy_auth import run_proxy_auth_group
from .groups.puppeteer import run_puppeteer_group
from .groups.stagehand import run_stagehand_group
from .groups.svg_rect import run_svg_rect_group
from .groups.target_semantics import run_target_semantics_group
from .groups.tracing import run_raw_tracing_group, run_tracing_group
from .groups.url_policy import run_url_policy_group
Expand Down Expand Up @@ -345,6 +346,12 @@ async def _await_group(group: SmokeGroup, awaitable: Awaitable[None]) -> None:
"browser",
run_webgl_viewport_group,
),
SmokeGroup(
"svg-rect",
"Detached SVGRect interface, float conversion, and SVG feature detection.",
"browser",
run_svg_rect_group,
),
SmokeGroup(
"media-error",
"HTMLMediaElement MediaError publication, identity, and reset lifecycle.",
Expand Down
1 change: 1 addition & 0 deletions moli-cdp-smoke/tests/test_group_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_default_runs_every_repository_managed_group(self) -> None:
self.assertIn("navigation-outcomes", DEFAULT_GROUP_NAMES)
self.assertIn("media-error", DEFAULT_GROUP_NAMES)
self.assertIn("webgl-viewport", DEFAULT_GROUP_NAMES)
self.assertIn("svg-rect", DEFAULT_GROUP_NAMES)
self.assertIn("multi-page", DEFAULT_GROUP_NAMES)
self.assertIn("puppeteer", DEFAULT_GROUP_NAMES)
self.assertEqual(
Expand Down
1 change: 1 addition & 0 deletions moli-renderer-v8/src/context_bootstrap/runtime_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,7 @@ pub(crate) fn finish_context_bootstrap(
("XPathResult", "XPathResult"),
("SVGLength", "SVGLength"),
("SVGNumber", "SVGNumber"),
("SVGRect", "SVGRect"),
("SVGAnimatedLength", "SVGAnimatedLength"),
("SVGLengthList", "SVGLengthList"),
("SVGAnimatedLengthList", "SVGAnimatedLengthList"),
Expand Down
5 changes: 5 additions & 0 deletions moli-renderer-v8/src/context_bootstrap/specs/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGRect",
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGAnimatedLength",
parent: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,13 @@ struct SvgTextContentElementTemplateMethodsDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGSVGElement", enumerable)]
struct SvgSvgElementTemplateMethodsDeclaration {
#[webapi(
method = "createSVGRect",
length = 0,
callback = super::rect::create_svg_rect
)]
create_svg_rect: (),

#[webapi(
method = "createSVGMatrix",
length = 0,
Expand Down
2 changes: 2 additions & 0 deletions moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use moli_svg::{
mod bindings;
mod builders;
mod callbacks;
mod rect;

const SVG_GRAPHICS_TRANSFORM_SLOT: &str = "__moliSvgGraphicsTransform";
const SVG_PATTERN_TRANSFORM_SLOT: &str = "__moliSvgPatternTransform";
Expand Down Expand Up @@ -244,6 +245,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>(
match name {
"SVGLength" => bindings::install_svg_length_bindings(scope, template),
"SVGNumber" => bindings::install_svg_number_bindings(scope, template),
"SVGRect" => rect::install_bindings(scope, template),
"SVGAnimatedLength" => bindings::install_svg_animated_length_bindings(scope, template),
"SVGLengthList" => bindings::install_svg_length_list_bindings(scope, template),
"SVGAnimatedLengthList" => {
Expand Down
139 changes: 139 additions & 0 deletions moli-renderer-v8/src/context_bootstrap/svg_runtime/rect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Detached SVGRect values created by SVGSVGElement.createSVGRect().
//!
//! JSXGraph uses this method to detect SVG support. Returning a DOMRect would
//! pass that check but expose the wrong interface and double (not float) fields.

use crate::{
native_bridge::node_runtime_and_handle_from_object_or_detached,
util::{callback_data_index_value, callback_data_item, get_private_value, set_private_value},
webidl,
};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};

const X: &str = "__moliSvgRectX";
const Y: &str = "__moliSvgRectY";
const WIDTH: &str = "__moliSvgRectWidth";
const HEIGHT: &str = "__moliSvgRectHeight";
const FIELDS: &[(&str, &str)] = &[("x", X), ("y", Y), ("width", WIDTH), ("height", HEIGHT)];

#[derive(WebApiObject)]
#[webapi(interface = "SVGRect")]
struct SvgRectObjectDeclaration {
#[webapi(slot = X)]
x: f64,
#[webapi(slot = Y)]
y: f64,
#[webapi(slot = WIDTH)]
width: f64,
#[webapi(slot = HEIGHT)]
height: f64,
}

#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGRect", enumerable)]
struct SvgRectAccessorsDeclaration {
#[webapi(accessor_property, getter = get_field, setter = set_field,
data = callback_data_index_value(scope, 0))]
x: (),
#[webapi(accessor_property, getter = get_field, setter = set_field,
data = callback_data_index_value(scope, 1))]
y: (),
#[webapi(accessor_property, getter = get_field, setter = set_field,
data = callback_data_index_value(scope, 2))]
width: (),
#[webapi(accessor_property, getter = get_field, setter = set_field,
data = callback_data_index_value(scope, 3))]
height: (),
}

pub(super) fn install_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let prototype = template.prototype_template(scope);
SvgRectAccessorsDeclaration::initialize_prototype_template(scope, prototype);
}

pub(super) fn create_svg_rect<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let is_svg_root = node_runtime_and_handle_from_object_or_detached(scope, args.this())
.ok()
.is_some_and(|(host, handle)| {
unsafe { &*host }
.dom_host()
.node(handle)
.and_then(crate::dom::native::Node::as_element)
.is_some_and(|element| element.is_svg_element("svg"))
});
if !is_svg_root {
webidl::throw_type_error(
scope,
"SVGSVGElement.createSVGRect called on incompatible receiver.",
);
return;
}
let rect = SvgRectObjectDeclaration::new(0.0, 0.0, 0.0, 0.0)
.bind(scope)
.expect("SVGRect declaration should bind");
rv.set(rect.into());
}

fn field_for_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
) -> Option<(&'static str, &'static str)> {
if get_private_value(scope, args.this(), X).is_none() {
webidl::throw_type_error(scope, "SVGRect accessor called on incompatible receiver.");
return None;
}
callback_data_item(scope, args, FIELDS, "SVGRect fields")
}

fn get_field<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some((_, slot)) = field_for_receiver(scope, &args) else {
return;
};
if let Some(value) = get_private_value(scope, args.this(), slot) {
rv.set(value);
}
}

fn set_field<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some((name, slot)) = field_for_receiver(scope, &args) else {
return;
};
let value = match webidl::convert::<webidl::Double>(
scope,
args.get(0),
webidl::Context::member("SVGRect", name),
) {
Ok(value) => value.0 as f32,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
// SVGRect uses restricted WebIDL float: round to binary32, reject both
// non-finite input and finite doubles which overflow that representation.
if !value.is_finite() {
webidl::throw_type_error(scope, "SVGRect value is outside the finite float range.");
return;
}
set_private_value(
scope,
args.this(),
slot,
v8::Number::new(scope, f64::from(value)).into(),
);
}
12 changes: 12 additions & 0 deletions moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
use super::*;

#[test]
fn svg_create_rect_supports_capability_detection_and_detached_float_values() {
let mut vm = new_storage_test_vm("https://svg-rect.test/");
assert_eq!(
vm.eval(include_str!(
"../../../../tests/fixtures/svg-create-rect.js"
))
.expect("SVGRect contract should pass"),
"svg-create-rect:ok"
);
}

async fn expect_one_child_frame_task_source(
vm: &mut ScriptVm,
expected: impl Into<ChildFrameSemanticTurnKind>,
Expand Down
47 changes: 47 additions & 0 deletions moli-renderer-v8/tests/fixtures/svg-create-rect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
(() => {
const check = (condition, message) => {
if (!condition) throw new Error(`createSVGRect: ${message}`);
};
const throwsTypeError = operation => {
try { operation(); } catch (error) { return error instanceof TypeError; }
return false;
};
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
check(typeof svg.createSVGRect === 'function' && svg.createSVGRect.length === 0, 'interface');
const rect = svg.createSVGRect();
const values = value => [value.x, value.y, value.width, value.height];
check(JSON.stringify(values(rect)) === '[0,0,0,0]', 'zero initialization');
check(rect instanceof SVGRect && !(rect instanceof DOMRect), 'SVGRect, not DOMRect');
check(Object.prototype.toString.call(rect) === '[object SVGRect]', 'prototype tag');
check(Object.getOwnPropertyNames(rect).length === 0, 'private value storage');
check(Object.getPrototypeOf(rect) === SVGRect.prototype, 'prototype identity');
check(Object.getPrototypeOf(SVGRect.prototype) === Object.prototype, 'prototype hierarchy');
check(throwsTypeError(() => new SVGRect()), 'illegal constructor');
check(throwsTypeError(() => svg.createSVGRect.call({})), 'method receiver');
check(throwsTypeError(() => svg.createSVGRect.call(
document.createElementNS('http://www.w3.org/2000/svg', 'g'))), 'non-root SVG receiver');
check(throwsTypeError(() => svg.createSVGRect.call(Object.create(SVGSVGElement.prototype))),
'forged SVG receiver');
for (const name of ['x', 'y', 'width', 'height']) {
const descriptor = Object.getOwnPropertyDescriptor(SVGRect.prototype, name);
check(descriptor.enumerable && descriptor.configurable, `${name} descriptor`);
check(typeof descriptor.get === 'function' && typeof descriptor.set === 'function', `${name} accessor`);
check(throwsTypeError(() => descriptor.get.call({})), `${name} getter receiver`);
check(throwsTypeError(() => descriptor.set.call({}, 1)), `${name} setter receiver`);
rect[name] = '1.2';
check(rect[name] === Math.fround(1.2), `${name} rounds to float`);
for (const invalid of [NaN, Infinity, -Infinity, 1e40, undefined, Symbol()]) {
check(throwsTypeError(() => { rect[name] = invalid; }), `${name} rejects non-finite float`);
check(rect[name] === Math.fround(1.2), `${name} rejected write preserves value`);
}
rect[name] = -5;
check(rect[name] === -5, `${name} accepts negative values`);
rect[name] = null;
check(rect[name] === 0, `${name} numeric conversion`);
}
const second = svg.createSVGRect();
rect.width = 50;
check(second !== rect && second.width === 0, 'independent detached values');
check(!svg.hasAttribute('width'), 'no live attribute binding');
return 'svg-create-rect:ok';
})()
Loading