fix(svg): enforce value list mutation semantics

This commit is contained in:
ldm0
2026-10-02 03:28:23 +08:00
parent f953b33d4e
commit b205dc2b8e
4 changed files with 423 additions and 45 deletions
@@ -209,6 +209,8 @@ struct SvgAnimatedTransformListObjectDeclaration<'scope> {
struct SvgLengthListObjectDeclaration<'scope> {
#[webapi(slot = SVG_LENGTH_LIST_ITEMS_SLOT)]
items: Vec<v8::Local<'scope, v8::Value>>,
#[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)]
read_only: bool,
#[webapi(method, callback = svg_length_list_clear_callback, length = 0)]
clear: (),
#[webapi(method, callback = svg_length_list_initialize_callback, length = 1)]
@@ -234,6 +236,8 @@ struct SvgLengthListObjectDeclaration<'scope> {
struct SvgNumberListObjectDeclaration<'scope> {
#[webapi(slot = SVG_NUMBER_LIST_ITEMS_SLOT)]
items: Vec<v8::Local<'scope, v8::Value>>,
#[webapi(slot = SVG_VALUE_LIST_READ_ONLY_SLOT)]
read_only: bool,
#[webapi(method, callback = svg_number_list_clear_callback, length = 0)]
clear: (),
#[webapi(method, callback = svg_number_list_initialize_callback, length = 1)]
@@ -451,8 +455,8 @@ pub(super) fn reflect_svg_animated_boolean_to_owner_attribute<'s>(
pub(super) fn build_svg_animated_length_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Object> {
let base_val = build_svg_length_list(scope);
let anim_val = build_svg_length_list(scope);
let base_val = build_svg_length_list(scope, false);
let anim_val = build_svg_length_list(scope, true);
SvgAnimatedLengthListObjectDeclaration::new(base_val, anim_val)
.bind(scope)
.expect("SVGAnimatedLengthList declaration should bind")
@@ -461,8 +465,8 @@ pub(super) fn build_svg_animated_length_list<'s>(
pub(super) fn build_svg_animated_number_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Object> {
let base_val = build_svg_number_list(scope);
let anim_val = build_svg_number_list(scope);
let base_val = build_svg_number_list(scope, false);
let anim_val = build_svg_number_list(scope, true);
SvgAnimatedNumberListObjectDeclaration::new(base_val, anim_val)
.bind(scope)
.expect("SVGAnimatedNumberList declaration should bind")
@@ -847,18 +851,32 @@ pub(super) fn serialize_svg_animated_enumeration(
pub(super) fn build_svg_length_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
read_only: bool,
) -> v8::Local<'s, v8::Object> {
SvgLengthListObjectDeclaration::new(Vec::new())
.bind(scope)
.expect("SVGLengthList declaration should bind")
let template = v8::ObjectTemplate::new(scope);
configure_svg_value_list_indexed_property_handler(template);
let object = template
.new_instance(scope)
.expect("SVGLengthList object template should instantiate");
SvgLengthListObjectDeclaration::new(Vec::new(), read_only)
.bind_into(scope, object)
.expect("SVGLengthList declaration should bind");
object
}
pub(super) fn build_svg_number_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
read_only: bool,
) -> v8::Local<'s, v8::Object> {
SvgNumberListObjectDeclaration::new(Vec::new())
.bind(scope)
.expect("SVGNumberList declaration should bind")
let template = v8::ObjectTemplate::new(scope);
configure_svg_value_list_indexed_property_handler(template);
let object = template
.new_instance(scope)
.expect("SVGNumberList object template should instantiate");
SvgNumberListObjectDeclaration::new(Vec::new(), read_only)
.bind_into(scope, object)
.expect("SVGNumberList declaration should bind");
object
}
pub(super) fn build_svg_string_list_for_attribute<'s>(
@@ -1724,15 +1742,34 @@ pub(super) fn svg_matrix_value_or_throw<'s>(
None
}
pub(super) fn svg_value_list_item_or_default<'s>(
pub(super) fn svg_value_list_item_or_throw<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
kind: SvgListKind,
) -> v8::Local<'s, v8::Object> {
v8::Local::<v8::Object>::try_from(value).unwrap_or_else(|_| match kind {
SvgListKind::Length => build_svg_length(scope, 0.0),
SvgListKind::Number => build_svg_number(scope, 0.0),
})
) -> Option<v8::Local<'s, v8::Object>> {
let (brand_slot, interface) = match kind {
SvgListKind::Length => (SVG_LENGTH_VALUE_SLOT, "SVGLength"),
SvgListKind::Number => (SVG_NUMBER_VALUE_SLOT, "SVGNumber"),
};
let object = v8::Local::<v8::Object>::try_from(value).ok();
if let Some(object) = object
&& get_private_value(scope, object, brand_slot).is_some()
{
return Some(object);
}
webidl::throw_type_error(
scope,
&format!("Argument 1 can not be converted to {interface}"),
);
None
}
pub(super) fn svg_value_list_is_read_only<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, object, SVG_VALUE_LIST_READ_ONLY_SLOT)
.is_some_and(|value| value.is_true())
}
pub(super) fn svg_value_list_items<'s>(
@@ -1865,27 +1902,47 @@ pub(super) fn sync_svg_animated_value_list_from_owner_attribute<'s>(
) {
let raw = svg_owner_attribute_value(scope, owner, attribute);
let raw_value = raw.clone().unwrap_or_default();
if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind)
&& svg_value_list_synced_attribute_value(scope, base_val)
.as_deref()
.is_some_and(|synced| synced == raw_value)
{
set_svg_value_list_owner_attribute(scope, base_val, owner, attribute);
return;
}
let base_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind);
let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind);
if let Some(base_val) = svg_animated_value_list_member(scope, animated, "baseVal", kind) {
set_svg_value_list_items(scope, base_val, base_items, kind);
set_svg_value_list_owner_attribute(scope, base_val, owner, attribute);
set_svg_value_list_synced_attribute_value(scope, base_val, &raw_value);
sync_svg_value_list_from_owner_attribute(scope, base_val, kind);
}
if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind) {
if let Some(anim_val) = svg_animated_value_list_member(scope, animated, "animVal", kind)
&& svg_value_list_synced_attribute_value(scope, anim_val).as_deref()
!= Some(raw_value.as_str())
{
let anim_items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind);
set_svg_value_list_items(scope, anim_val, anim_items, kind);
set_svg_value_list_synced_attribute_value(scope, anim_val, &raw_value);
}
}
pub(super) fn sync_svg_value_list_from_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
list: v8::Local<'s, v8::Object>,
kind: SvgListKind,
) {
let Some(owner) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ELEMENT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Some(attribute) = get_private_value(scope, list, SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT)
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
.filter(|value| !value.is_empty())
else {
return;
};
let raw = svg_owner_attribute_value(scope, owner, &attribute);
let raw_value = raw.clone().unwrap_or_default();
if svg_value_list_synced_attribute_value(scope, list).as_deref() == Some(raw_value.as_str()) {
return;
}
let items = build_svg_value_list_items_from_attribute(scope, raw.as_deref(), kind);
set_svg_value_list_items(scope, list, items, kind);
set_svg_value_list_synced_attribute_value(scope, list, &raw_value);
}
pub(super) fn build_svg_value_list_items_from_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
raw: Option<&str>,
@@ -34,6 +34,21 @@ pub(super) fn configure_svg_string_list_indexed_property_handler(
);
}
pub(super) fn configure_svg_value_list_indexed_property_handler(
template: v8::Local<'_, v8::ObjectTemplate>,
) {
template.set_indexed_property_handler(
v8::IndexedPropertyHandlerConfiguration::new()
.getter(svg_value_list_indexed_getter)
.setter(svg_value_list_indexed_setter)
.query(svg_value_list_indexed_query)
.deleter(svg_value_list_indexed_deleter)
.enumerator(svg_value_list_indexed_enumerator)
.definer(svg_value_list_indexed_definer)
.descriptor(svg_value_list_indexed_descriptor),
);
}
fn svg_dom_matrix_2d_init_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
@@ -1946,12 +1961,56 @@ fn svg_string_list_indexed_descriptor<'s>(
v8::Intercepted::kYes
}
fn svg_value_list_metadata(kind: SvgListKind) -> (&'static str, &'static str) {
match kind {
SvgListKind::Length => (SVG_LENGTH_LIST_ITEMS_SLOT, "SVGLengthList"),
SvgListKind::Number => (SVG_NUMBER_LIST_ITEMS_SLOT, "SVGNumberList"),
}
}
fn svg_value_list_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
list: v8::Local<'s, v8::Object>,
) -> Option<SvgListKind> {
if web_api_interfaces::SVGLengthList::is_instance(scope, list) {
Some(SvgListKind::Length)
} else if web_api_interfaces::SVGNumberList::is_instance(scope, list) {
Some(SvgListKind::Number)
} else {
None
}
}
fn require_svg_value_list_items<'s>(
scope: &mut v8::PinScope<'s, '_>,
list: v8::Local<'s, v8::Object>,
kind: SvgListKind,
member: &str,
writable: bool,
) -> Option<v8::Local<'s, v8::Array>> {
let (_, interface) = svg_value_list_metadata(kind);
if !require_svg_receiver(scope, list, interface, member) {
return None;
}
if writable && svg_value_list_is_read_only(scope, list) {
throw_dom_exception(
scope,
"NoModificationAllowedError",
7,
"The SVG value list is read-only.",
);
return None;
}
sync_svg_value_list_from_owner_attribute(scope, list, kind);
Some(svg_value_list_items(scope, list, kind))
}
pub(super) fn svg_length_list_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
svg_value_list_length_getter(scope, args, rv, SvgListKind::Length, "SVGLengthList");
svg_value_list_length_getter(scope, args, rv, SvgListKind::Length);
}
pub(super) fn svg_number_list_length_getter<'s>(
@@ -1959,7 +2018,7 @@ pub(super) fn svg_number_list_length_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
svg_value_list_length_getter(scope, args, rv, SvgListKind::Number, "SVGNumberList");
svg_value_list_length_getter(scope, args, rv, SvgListKind::Number);
}
pub(super) fn svg_value_list_length_getter<'s>(
@@ -1967,13 +2026,13 @@ pub(super) fn svg_value_list_length_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
interface: &'static str,
) {
if !require_svg_receiver(scope, args.this(), interface, "length getter") {
let Some(items) =
require_svg_value_list_items(scope, args.this(), kind, "length getter", false)
else {
return;
}
let length = svg_value_list_items(scope, args.this(), kind).length();
rv.set(v8::Integer::new_from_unsigned(scope, length).into());
};
rv.set(v8::Integer::new_from_unsigned(scope, items.length()).into());
}
pub(super) fn svg_length_list_clear_callback<'s>(
@@ -1998,6 +2057,9 @@ pub(super) fn svg_value_list_clear_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
if require_svg_value_list_items(scope, args.this(), kind, "clear", true).is_none() {
return;
}
set_svg_value_list_items(scope, args.this(), v8::Array::new(scope, 0), kind);
reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind);
rv.set_undefined();
@@ -2025,10 +2087,15 @@ pub(super) fn svg_value_list_initialize_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
if require_svg_value_list_items(scope, args.this(), kind, "initialize", true).is_none() {
return;
}
let Some(parsed) = webidl::parse_args::<SvgListItemArgs>(scope, &args) else {
return;
};
let item = svg_value_list_item_or_default(scope, parsed.item, kind);
let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else {
return;
};
let Some(items) = serialize_v8_array(scope, [item]) else {
return;
};
@@ -2059,10 +2126,13 @@ pub(super) fn svg_value_list_get_item_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "getItem", false)
else {
return;
};
let Some(parsed) = webidl::parse_args::<SvgListIndexArgs>(scope, &args) else {
return;
};
let items = svg_value_list_items(scope, args.this(), kind);
let Some(item) = svg_list_item_or_throw(scope, items, parsed.index) else {
return;
};
@@ -2091,11 +2161,17 @@ pub(super) fn svg_value_list_insert_item_before_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
let Some(items) =
require_svg_value_list_items(scope, args.this(), kind, "insertItemBefore", true)
else {
return;
};
let Some(parsed) = webidl::parse_args::<SvgListItemIndexArgs>(scope, &args) else {
return;
};
let item = svg_value_list_item_or_default(scope, parsed.item, kind);
let items = svg_value_list_items(scope, args.this(), kind);
let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else {
return;
};
let length = items.length();
let index = parsed.index.min(length);
let next = v8::Array::new(scope, (length + 1) as i32);
@@ -2137,15 +2213,20 @@ pub(super) fn svg_value_list_replace_item_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "replaceItem", true)
else {
return;
};
let Some(parsed) = webidl::parse_args::<SvgListItemIndexArgs>(scope, &args) else {
return;
};
let items = svg_value_list_items(scope, args.this(), kind);
let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else {
return;
};
if parsed.index >= items.length() {
webidl::throw_index_size_error(scope);
return;
}
let item = svg_value_list_item_or_default(scope, parsed.item, kind);
if let Some(replaced) = items
.get_index(scope, parsed.index)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
@@ -2182,11 +2263,14 @@ pub(super) fn svg_value_list_remove_item_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "removeItem", true)
else {
return;
};
let Some(parsed) = webidl::parse_args::<SvgListIndexArgs>(scope, &args) else {
return;
};
let index = parsed.index;
let items = svg_value_list_items(scope, args.this(), kind);
let length = items.length();
if index >= length {
webidl::throw_index_size_error(scope);
@@ -2236,17 +2320,168 @@ pub(super) fn svg_value_list_append_item_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
kind: SvgListKind,
) {
let Some(items) = require_svg_value_list_items(scope, args.this(), kind, "appendItem", true)
else {
return;
};
let Some(parsed) = webidl::parse_args::<SvgListItemArgs>(scope, &args) else {
return;
};
let item = svg_value_list_item_or_default(scope, parsed.item, kind);
let items = svg_value_list_items(scope, args.this(), kind);
let Some(item) = svg_value_list_item_or_throw(scope, parsed.item, kind) else {
return;
};
set_svg_value_list_item_owner_list(scope, item, args.this());
let _ = items.set_index(scope, items.length(), item.into());
reflect_svg_value_list_to_owner_attribute(scope, args.this(), kind);
rv.set(item.into());
}
fn svg_value_list_intercepted_items<'s>(
scope: &mut v8::PinScope<'s, '_>,
list: v8::Local<'s, v8::Object>,
) -> Option<(SvgListKind, v8::Local<'s, v8::Array>)> {
let kind = svg_value_list_kind(scope, list)?;
sync_svg_value_list_from_owner_attribute(scope, list, kind);
Some((kind, svg_value_list_items(scope, list, kind)))
}
fn svg_value_list_indexed_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(value) = svg_value_list_intercepted_items(scope, args.holder())
.map(|(_, items)| items)
.filter(|items| index < items.length())
.and_then(|items| items.get_index(scope, index))
else {
return v8::Intercepted::kNo;
};
rv.set(value);
v8::Intercepted::kYes
}
fn svg_value_list_indexed_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
value: v8::Local<'s, v8::Value>,
args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let list = args.holder();
let Some(kind) = svg_value_list_kind(scope, list) else {
return v8::Intercepted::kNo;
};
let Some(items) = require_svg_value_list_items(scope, list, kind, "indexed setter", true)
else {
return v8::Intercepted::kYes;
};
let Some(item) = svg_value_list_item_or_throw(scope, value, kind) else {
return v8::Intercepted::kYes;
};
if index >= items.length() {
webidl::throw_index_size_error(scope);
return v8::Intercepted::kYes;
}
if let Some(replaced) = items
.get_index(scope, index)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
clear_svg_value_list_item_owner_list(scope, replaced);
}
let _ = items.set_index(scope, index, item.into());
set_svg_value_list_item_owner_list(scope, item, list);
reflect_svg_value_list_to_owner_attribute(scope, list, kind);
v8::Intercepted::kYes
}
fn svg_value_list_indexed_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if index >= items.length() {
return v8::Intercepted::kNo;
}
rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32);
v8::Intercepted::kYes
}
fn svg_value_list_indexed_deleter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let Some((_, items)) = svg_value_list_intercepted_items(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if index >= items.length() {
return v8::Intercepted::kNo;
}
rv.set_bool(false);
v8::Intercepted::kYes
}
fn svg_value_list_indexed_definer<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
descriptor: &v8::PropertyDescriptor,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
if descriptor.has_get() || descriptor.has_set() {
rv.set_bool(false);
return v8::Intercepted::kYes;
}
let value = if descriptor.has_value() {
v8::Local::new(scope, descriptor.value())
} else {
v8::undefined(scope).into()
};
svg_value_list_indexed_setter(scope, index, value, args, rv)
}
fn svg_value_list_indexed_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let length = svg_value_list_intercepted_items(scope, args.holder())
.map(|(_, items)| items.length())
.unwrap_or(0);
let keys = (0..length)
.map(|index| v8::Integer::new_from_unsigned(scope, index).into())
.collect::<Vec<_>>();
rv.set(v8::Array::new_with_elements(scope, &keys));
}
fn svg_value_list_indexed_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(value) = svg_value_list_intercepted_items(scope, args.holder())
.map(|(_, items)| items)
.filter(|items| index < items.length())
.and_then(|items| items.get_index(scope, index))
else {
return v8::Intercepted::kNo;
};
let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, true, true).bind(scope)
else {
return v8::Intercepted::kNo;
};
rv.set(descriptor.into());
v8::Intercepted::kYes
}
pub(super) fn svg_transform_list_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -79,6 +79,7 @@ const SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT: &str =
const SVG_VALUE_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgValueListOwnerElement";
const SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgValueListOwnerAttribute";
const SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT: &str = "__moliSvgValueListItemOwnerList";
const SVG_VALUE_LIST_READ_ONLY_SLOT: &str = "__moliSvgValueListReadOnly";
const SVG_VALUE_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT: &str = "__moliSvgValueListSyncedAttributeValue";
const SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationBaseVal";
const SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedEnumerationAnimVal";
@@ -3017,3 +3017,88 @@ fn svg_href_animated_string_prefers_href_and_falls_back_to_xlink_href() {
assert_eq!(result, "ok");
}
#[test]
fn svg_value_lists_enforce_item_types_indices_and_read_only_anim_values() {
let mut vm = new_storage_test_vm("https://svg-value-list-semantics.test/");
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const errorName = callback => {
try {
callback();
return "none";
} catch (error) {
return error.name;
}
};
const SVG_NS = "http://www.w3.org/2000/svg";
const text = document.createElementNS(SVG_NS, "text");
const svg = document.createElementNS(SVG_NS, "svg");
text.setAttribute("x", "10 20");
text.setAttribute("rotate", "15 30");
const lengths = text.x;
const lengthBase = lengths.baseVal;
const lengthAnim = lengths.animVal;
const numbers = text.rotate;
const numberBase = numbers.baseVal;
const numberAnim = numbers.animVal;
const invalidItems = [30, "invalid", text, null];
for (const item of invalidItems) {
assert(errorName(() => lengthBase.initialize(item)) === "TypeError",
"SVGLengthList.initialize item type");
assert(errorName(() => lengthBase.insertItemBefore(item, 0)) === "TypeError",
"SVGLengthList.insertItemBefore item type");
assert(errorName(() => lengthBase.replaceItem(item, 0)) === "TypeError",
"SVGLengthList.replaceItem item type");
assert(errorName(() => lengthBase.appendItem(item)) === "TypeError",
"SVGLengthList.appendItem item type");
assert(errorName(() => { lengthBase[0] = item; }) === "TypeError",
"SVGLengthList indexed setter item type");
}
const length = svg.createSVGLength();
length.value = 42;
lengthBase[0] = length;
assert(lengthBase[0] === length, "SVGLengthList indexed getter");
assert(text.getAttribute("x") === "42 20", "SVGLengthList indexed reflection");
const number = svg.createSVGNumber();
number.value = 7;
numberBase[1] = number;
assert(numberBase[1] === number, "SVGNumberList indexed getter");
assert(text.getAttribute("rotate") === "15 7", "SVGNumberList indexed reflection");
assert(errorName(() => lengthBase.appendItem(number)) === "TypeError",
"SVGLengthList rejects SVGNumber");
assert(errorName(() => numberBase.appendItem(length)) === "TypeError",
"SVGNumberList rejects SVGLength");
text.setAttribute("x", "1 2 3");
assert(lengthBase.length === 3 && lengthBase[2].value === 3,
"saved baseVal resynchronizes");
assert(text.x.animVal.length === 3 && text.x.animVal[2].value === 3,
"animVal resynchronizes after direct baseVal access");
assert(errorName(() => lengthAnim.clear()) === "NoModificationAllowedError",
"SVGLengthList animVal clear");
assert(errorName(() => { lengthAnim[0] = length; }) === "NoModificationAllowedError",
"SVGLengthList animVal indexed setter");
assert(errorName(() => numberAnim.appendItem(number)) === "NoModificationAllowedError",
"SVGNumberList animVal appendItem");
assert(errorName(() => SVGLengthList.prototype.clear.call({})) === "TypeError",
"SVGLengthList receiver brand");
return "ok";
})()
"#,
)
.expect("SVG value list semantics probe should evaluate");
assert_eq!(result, "ok");
}