diff --git a/moli-renderer-v8/src/native_bridge/bridge/wrappers.rs b/moli-renderer-v8/src/native_bridge/bridge/wrappers.rs index e58a22032..8b8696f25 100644 --- a/moli-renderer-v8/src/native_bridge/bridge/wrappers.rs +++ b/moli-renderer-v8/src/native_bridge/bridge/wrappers.rs @@ -88,11 +88,7 @@ impl NativeDomBridge { runtime_ptr: *mut JsContextHost, handle: DomHandle, ) -> Option> { - self.wrap_bridge_handle( - scope, - runtime_ptr, - BridgeHandle::ClassList(handle, DomTokenListKind::Class), - ) + self.wrap_dom_token_list(scope, runtime_ptr, handle, DomTokenListKind::Class) } pub(crate) fn wrap_part_list<'s, 'i>( @@ -101,24 +97,17 @@ impl NativeDomBridge { runtime_ptr: *mut JsContextHost, handle: DomHandle, ) -> Option> { - self.wrap_bridge_handle( - scope, - runtime_ptr, - BridgeHandle::ClassList(handle, DomTokenListKind::Part), - ) + self.wrap_dom_token_list(scope, runtime_ptr, handle, DomTokenListKind::Part) } - pub(crate) fn wrap_rel_list<'s, 'i>( + pub(in crate::native_bridge) fn wrap_dom_token_list<'s, 'i>( &mut self, scope: &mut v8::PinScope<'s, 'i>, runtime_ptr: *mut JsContextHost, handle: DomHandle, + kind: DomTokenListKind, ) -> Option> { - self.wrap_bridge_handle( - scope, - runtime_ptr, - BridgeHandle::ClassList(handle, DomTokenListKind::Rel), - ) + self.wrap_bridge_handle(scope, runtime_ptr, BridgeHandle::ClassList(handle, kind)) } pub(crate) fn wrap_dataset<'s, 'i>( diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 734ccd146..c1af7f6e6 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -171,7 +171,9 @@ pub(crate) use canvas::{ pub(crate) use class_list::install_dom_token_list_prototype_bindings; pub(super) use class_list::{ build_dom_token_list_wrapper_template, html_rel_list_getter_function, - html_rel_list_setter_function, + html_rel_list_setter_function, iframe_sandbox_getter_function, iframe_sandbox_setter_function, + link_sizes_getter_function, link_sizes_setter_function, output_html_for_getter_function, + output_html_for_setter_function, svg_rel_list_setter_function, }; pub(super) use content::{ node_direct_text_content, node_get_html_callback, node_inner_html_getter_function, @@ -461,10 +463,9 @@ pub(super) use global_attributes::{ node_hidden_setter_function, node_inert_getter_function, node_inert_setter_function, node_input_mode_getter_function, node_input_mode_setter_function, node_is_content_editable_getter_function, node_lang_getter_function, node_lang_setter_function, - node_sandbox_getter_function, node_sandbox_setter_function, node_spellcheck_getter_function, - node_spellcheck_setter_function, node_tab_index_getter_function, - node_tab_index_setter_function, node_title_getter_function, node_title_setter_function, - node_translate_getter_function, node_translate_setter_function, + node_spellcheck_getter_function, node_spellcheck_setter_function, + node_tab_index_getter_function, node_tab_index_setter_function, node_title_getter_function, + node_title_setter_function, node_translate_getter_function, node_translate_setter_function, node_writing_suggestions_getter_function, node_writing_suggestions_setter_function, null_to_empty_dom_string_reflection_getter_function, null_to_empty_dom_string_reflection_setter_function, object_archive_getter_function, @@ -4112,8 +4113,8 @@ struct HtmlIFrameElementPrototypeDeclaration { referrer_policy: (), #[webapi( accessor_property, - getter = node_sandbox_getter_function, - setter = node_sandbox_setter_function + getter = iframe_sandbox_getter_function, + setter = iframe_sandbox_setter_function )] sandbox: (), #[webapi( @@ -4252,6 +4253,12 @@ struct HtmlLinkElementUrlPrototypeDeclaration { setter_data = DomStringReflection::LinkHreflang )] hreflang: (), + #[webapi( + accessor_property, + getter = link_sizes_getter_function, + setter = link_sizes_setter_function + )] + sizes: (), #[webapi( accessor_property, getter = html_charset_getter_function, @@ -5086,6 +5093,12 @@ struct HtmlInputElementValuePrototypeDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::HTMLOutputElement, enumerable)] struct HtmlOutputElementValuePrototypeDeclaration { + #[webapi( + accessor_property = "htmlFor", + getter = output_html_for_getter_function, + setter = output_html_for_setter_function + )] + html_for: (), #[webapi( accessor_property, getter = output_default_value_getter_function, @@ -5102,6 +5115,17 @@ struct HtmlOutputElementValuePrototypeDeclaration { type_: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGAElement, enumerable)] +struct SvgAElementRelListPrototypeDeclaration { + #[webapi( + accessor_property = "relList", + getter = html_rel_list_getter_function, + setter = svg_rel_list_setter_function + )] + rel_list: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::HTMLMeterElement, enumerable)] struct HtmlMeterElementPrototypeDeclaration { @@ -7374,6 +7398,7 @@ pub(crate) fn install_element_template_bindings<'s>( "HTMLScriptElement" => install!(HtmlScriptElementPrototypeDeclaration), "SVGScriptElement" => install!(SvgScriptElementPrototypeDeclaration), "SVGImageElement" => install!(SvgImageElementPrototypeDeclaration), + "SVGAElement" => install!(SvgAElementRelListPrototypeDeclaration), "HTMLStyleElement" => install!(HtmlStyleElementPrototypeDeclaration), "SVGStyleElement" => install!(SvgStyleElementPrototypeDeclaration), "HTMLTableElement" => install!(HtmlTableElementPrototypeDeclaration), diff --git a/moli-renderer-v8/src/native_bridge/element/class_list.rs b/moli-renderer-v8/src/native_bridge/element/class_list.rs index 01392deac..8aa2f9970 100644 --- a/moli-renderer-v8/src/native_bridge/element/class_list.rs +++ b/moli-renderer-v8/src/native_bridge/element/class_list.rs @@ -15,7 +15,9 @@ mod template; mod tokens; pub(in crate::native_bridge) use self::accessors::{ - html_rel_list_getter_function, html_rel_list_setter_function, + html_rel_list_getter_function, html_rel_list_setter_function, iframe_sandbox_getter_function, + iframe_sandbox_setter_function, link_sizes_getter_function, link_sizes_setter_function, + output_html_for_getter_function, output_html_for_setter_function, svg_rel_list_setter_function, }; pub(in crate::native_bridge) use self::template::build_dom_token_list_wrapper_template; pub(crate) use self::template::install_dom_token_list_prototype_bindings; diff --git a/moli-renderer-v8/src/native_bridge/element/class_list/accessors.rs b/moli-renderer-v8/src/native_bridge/element/class_list/accessors.rs index 4ebf64e5a..4071aea62 100644 --- a/moli-renderer-v8/src/native_bridge/element/class_list/accessors.rs +++ b/moli-renderer-v8/src/native_bridge/element/class_list/accessors.rs @@ -1,9 +1,10 @@ use super::*; -pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>( +fn token_list_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, + kind: DomTokenListKind, ) { let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, args.this()) @@ -16,30 +17,41 @@ pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>( return; } let runtime = unsafe { &mut *runtime_ptr }; - match runtime - .native_bridge_mut() - .wrap_rel_list(scope, runtime_ptr, handle) - { - Some(rel_list) => rv.set(rel_list.into()), + let token_list = + runtime + .native_bridge_mut() + .wrap_dom_token_list(scope, runtime_ptr, handle, kind); + match token_list { + Some(token_list) => rv.set(token_list.into()), None => rv.set_null(), } } -fn set_html_rel_list_for_receiver<'s>( +fn set_token_list_for_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, owner: &'static str, + property: &'static str, + attribute: &'static str, ) { let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, receiver) else { return; }; - let Some(value) = property_dom_string_value(scope, value, owner, "relList") else { + let Some(value) = property_dom_string_value(scope, value, owner, property) else { return; }; - set_reflected_attribute(scope, runtime_ptr, handle, "rel", &value); + set_reflected_attribute(scope, runtime_ptr, handle, attribute, &value); +} + +pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + token_list_getter(scope, args, rv, DomTokenListKind::Rel); } pub(in crate::native_bridge) fn html_rel_list_setter_function<'s>( @@ -50,7 +62,102 @@ pub(in crate::native_bridge) fn html_rel_list_setter_function<'s>( if let Some(interface) = super::super::reflection::ElementReflectionInterface::from_callback_data(scope, args.data()) { - set_html_rel_list_for_receiver(scope, args.this(), args.get(0), interface.name()); + set_token_list_for_receiver( + scope, + args.this(), + args.get(0), + interface.name(), + "relList", + "rel", + ); } rv.set_undefined(); } + +pub(in crate::native_bridge) fn svg_rel_list_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + set_token_list_for_receiver( + scope, + args.this(), + args.get(0), + "SVGAElement", + "relList", + "rel", + ); + rv.set_undefined(); +} + +pub(in crate::native_bridge) fn output_html_for_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + token_list_getter(scope, args, rv, DomTokenListKind::HtmlFor); +} + +pub(in crate::native_bridge) fn output_html_for_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + set_token_list_for_receiver( + scope, + args.this(), + args.get(0), + "HTMLOutputElement", + "htmlFor", + "for", + ); + rv.set_undefined(); +} + +pub(in crate::native_bridge) fn iframe_sandbox_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + token_list_getter(scope, args, rv, DomTokenListKind::Sandbox); +} + +pub(in crate::native_bridge) fn iframe_sandbox_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + set_token_list_for_receiver( + scope, + args.this(), + args.get(0), + "HTMLIFrameElement", + "sandbox", + "sandbox", + ); + rv.set_undefined(); +} + +pub(in crate::native_bridge) fn link_sizes_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + token_list_getter(scope, args, rv, DomTokenListKind::Sizes); +} + +pub(in crate::native_bridge) fn link_sizes_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + set_token_list_for_receiver( + scope, + args.this(), + args.get(0), + "HTMLLinkElement", + "sizes", + "sizes", + ); + rv.set_undefined(); +} diff --git a/moli-renderer-v8/src/native_bridge/element/class_list/methods.rs b/moli-renderer-v8/src/native_bridge/element/class_list/methods.rs index 42c247dcf..e14f61f7c 100644 --- a/moli-renderer-v8/src/native_bridge/element/class_list/methods.rs +++ b/moli-renderer-v8/src/native_bridge/element/class_list/methods.rs @@ -72,12 +72,34 @@ const LINK_REL_LIST_SUPPORTED_TOKENS: &[&str] = &[ const NAVIGATION_REL_LIST_SUPPORTED_TOKENS: &[&str] = &["noreferrer", "noopener", "opener"]; +const SANDBOX_SUPPORTED_TOKENS: &[&str] = &[ + "allow-downloads", + "allow-forms", + "allow-modals", + "allow-orientation-lock", + "allow-pointer-lock", + "allow-popups", + "allow-popups-to-escape-sandbox", + "allow-presentation", + "allow-same-origin", + "allow-scripts", + "allow-storage-access-by-user-activation", + "allow-top-navigation", + "allow-top-navigation-by-user-activation", + "allow-top-navigation-to-custom-protocols", +]; + fn rel_list_supports_token(runtime: &JsContextHost, handle: DomHandle, token: &str) -> bool { let supported_tokens = if runtime.dom_host().is_html_element_named(handle, "link") { LINK_REL_LIST_SUPPORTED_TOKENS } else if ["a", "area", "form"] .into_iter() .any(|name| runtime.dom_host().is_html_element_named(handle, name)) + || runtime + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.is_svg_element("a")) { NAVIGATION_REL_LIST_SUPPORTED_TOKENS } else { @@ -88,6 +110,12 @@ fn rel_list_supports_token(runtime: &JsContextHost, handle: DomHandle, token: &s .any(|supported| token.eq_ignore_ascii_case(supported)) } +fn sandbox_supports_token(token: &str) -> bool { + SANDBOX_SUPPORTED_TOKENS + .iter() + .any(|supported| token.eq_ignore_ascii_case(supported)) +} + pub(super) fn class_list_item_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -314,11 +342,18 @@ pub(super) fn class_list_supports_callback<'s>( return; }; match kind { - DomTokenListKind::Class | DomTokenListKind::Part => { + DomTokenListKind::Class + | DomTokenListKind::Part + | DomTokenListKind::HtmlFor + | DomTokenListKind::Sizes => { throw_type_error(scope, "DOMTokenList has no supported tokens."); return; } DomTokenListKind::Rel => {} + DomTokenListKind::Sandbox => { + rv.set_bool(sandbox_supports_token(&parsed.token)); + return; + } } rv.set_bool(rel_list_supports_token( unsafe { &*runtime_ptr }, diff --git a/moli-renderer-v8/src/native_bridge/element/class_list/tokens.rs b/moli-renderer-v8/src/native_bridge/element/class_list/tokens.rs index 8816753f2..2ef70061d 100644 --- a/moli-renderer-v8/src/native_bridge/element/class_list/tokens.rs +++ b/moli-renderer-v8/src/native_bridge/element/class_list/tokens.rs @@ -40,5 +40,8 @@ pub(super) fn token_list_attribute_name(kind: DomTokenListKind) -> &'static str DomTokenListKind::Class => "class", DomTokenListKind::Part => "part", DomTokenListKind::Rel => "rel", + DomTokenListKind::HtmlFor => "for", + DomTokenListKind::Sandbox => "sandbox", + DomTokenListKind::Sizes => "sizes", } } diff --git a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs index 6f53c4d71..fac318160 100644 --- a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs +++ b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs @@ -1030,30 +1030,6 @@ pub(in crate::native_bridge) fn html_color_getter_function<'s>( attribute_property_getter_from_object_or_detached(scope, args.this(), "color", rv); } -pub(in crate::native_bridge) fn node_sandbox_getter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - rv: v8::ReturnValue<'s, v8::Value>, -) { - attribute_property_getter_from_object_or_detached(scope, args.this(), "sandbox", rv); -} - -pub(in crate::native_bridge) fn node_sandbox_setter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'s, v8::Value>, -) { - set_dom_string_attribute_property_on_object( - scope, - args.this(), - "sandbox", - args.get(0), - "HTMLIFrameElement", - "sandbox", - ); - rv.set_undefined(); -} - fn set_dom_string_treat_null_as_empty_on_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index 69c821d90..3b51081bf 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -150,6 +150,9 @@ pub(super) enum DomTokenListKind { Class, Part, Rel, + HtmlFor, + Sandbox, + Sizes, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached/legacy_resources_and_document_mutations.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached/legacy_resources_and_document_mutations.rs index 2ca6a6049..26d541d30 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached/legacy_resources_and_document_mutations.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached/legacy_resources_and_document_mutations.rs @@ -1034,10 +1034,14 @@ fn detached_resource_template_accessors_use_owner_prototypes() { } assert(iframe.csp === "", "iframe csp default"); iframe.csp = 123456; + const sandbox = iframe.sandbox; + assert(Object.prototype.toString.call(sandbox) === "[object DOMTokenList]", "iframe sandbox type"); + assert(sandbox === iframe.sandbox, "iframe sandbox SameObject"); iframe.sandbox = "allow-scripts"; iframe.allowFullscreen = true; assert(iframe.csp === "123456" && iframe.getAttribute("csp") === "123456", "iframe csp"); - assert(iframe.sandbox === "allow-scripts" && iframe.getAttribute("sandbox") === "allow-scripts", "iframe sandbox"); + assert(sandbox.value === "allow-scripts" && iframe.getAttribute("sandbox") === "allow-scripts", "iframe sandbox"); + assert(sandbox.supports("ALLOW-SCRIPTS"), "iframe sandbox supported token"); assert(iframe.allowFullscreen === true && iframe.getAttribute("allowfullscreen") === "", "iframe allowFullscreen"); for (const name of ["csp", "sandbox", "allowFullscreen"]) { assert(!own(iframe, name), `iframe.${name} should stay inherited after set`); @@ -1046,7 +1050,7 @@ fn detached_resource_template_accessors_use_owner_prototypes() { } iframe.setAttribute("csp", "default-src 'self'"); assert(iframe.csp === "default-src 'self'", "iframe csp after delete"); - assert(iframe.sandbox === "allow-scripts", "iframe sandbox after delete"); + assert(iframe.sandbox === sandbox && sandbox.value === "allow-scripts", "iframe sandbox after delete"); assert(iframe.allowFullscreen === true, "iframe allowFullscreen after delete"); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs index e8c5cfbb3..11543174d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs @@ -2732,3 +2732,101 @@ fn element_heading_reflections_drive_flat_tree_heading_matching() { r#"{"owner":[true,false,true,true],"initial":[0,false,true],"parentOffset":["3",true],"reset":[true,true],"clamped":["20",8,true],"slotted":true,"modalState":[false,true,true],"badGetter":"throw:TypeError","badSetter":"throw:TypeError"}"# ); } + +#[test] +fn reflected_dom_token_list_attributes_are_live_same_object_and_owner_scoped() { + let mut vm = new_storage_test_vm("https://reflected-token-lists.test/"); + + let result = vm + .eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const svg = "http://www.w3.org/2000/svg"; + const detached = document.implementation.createHTMLDocument(""); + const descriptors = [ + [HTMLIFrameElement.prototype, "sandbox"], + [HTMLLinkElement.prototype, "sizes"], + [HTMLOutputElement.prototype, "htmlFor"], + [SVGAElement.prototype, "relList"] + ]; + for (const [prototype, name] of descriptors) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); + assert(!!descriptor, `${prototype.constructor.name}.${name} descriptor`); + assert(typeof descriptor.get === "function", `${name} getter`); + assert(typeof descriptor.set === "function", `${name} PutForwards setter`); + assert(descriptor.enumerable && descriptor.configurable, `${name} descriptor flags`); + } + + const noSupportedTokens = list => { + try { + list.supports("anything"); + return false; + } catch (error) { + return error && error.name === "TypeError"; + } + }; + const exercise = ownerDocument => { + const iframe = ownerDocument.createElement("iframe"); + const sandbox = iframe.sandbox; + assert(Object.prototype.toString.call(sandbox) === "[object DOMTokenList]", "sandbox type"); + assert(sandbox === iframe.sandbox, "sandbox SameObject"); + iframe.sandbox = "allow-scripts allow-forms allow-scripts"; + assert(sandbox.length === 2 && sandbox.contains("allow-forms"), "sandbox tokens"); + assert(iframe.getAttribute("sandbox") === "allow-scripts allow-forms allow-scripts", "sandbox PutForwards"); + assert(sandbox.supports("ALLOW-SCRIPTS"), "sandbox supports ASCII case-insensitively"); + assert(sandbox.supports("allow-storage-access-by-user-activation"), "sandbox storage-access token"); + assert(!sandbox.supports("unknown"), "sandbox rejects unknown token"); + sandbox.add("allow-popups"); + assert(iframe.getAttribute("sandbox") === "allow-scripts allow-forms allow-popups", "sandbox mutation reflects"); + + const output = ownerDocument.createElement("output"); + const htmlFor = output.htmlFor; + assert(Object.prototype.toString.call(htmlFor) === "[object DOMTokenList]", "htmlFor type"); + assert(htmlFor === output.htmlFor, "htmlFor SameObject"); + output.htmlFor = "first second first"; + htmlFor.add("third"); + assert(output.getAttribute("for") === "first second third", "htmlFor reflects for"); + assert(noSupportedTokens(htmlFor), "htmlFor has no supported tokens"); + + const link = ownerDocument.createElement("link"); + const sizes = link.sizes; + assert(Object.prototype.toString.call(sizes) === "[object DOMTokenList]", "sizes type"); + assert(sizes === link.sizes, "sizes SameObject"); + assert(sizes !== link.relList, "link token lists have distinct identity"); + link.sizes = "16x16 32x32 16x16"; + sizes.remove("16x16"); + assert(link.getAttribute("sizes") === "32x32", "sizes mutation reflects"); + assert(noSupportedTokens(sizes), "sizes has no supported tokens"); + + const anchor = ownerDocument.createElementNS(svg, "a"); + const relList = anchor.relList; + assert(Object.prototype.toString.call(relList) === "[object DOMTokenList]", "SVG relList type"); + assert(relList === anchor.relList, "SVG relList SameObject"); + anchor.relList = "noopener noreferrer"; + relList.add("opener"); + assert(anchor.getAttribute("rel") === "noopener noreferrer opener", "SVG relList reflects rel"); + assert(relList.supports("NOOPENER"), "SVG relList supported tokens"); + }; + + exercise(document); + exercise(detached); + + const div = document.createElement("div"); + for (const name of ["htmlFor", "sandbox", "sizes", "relList"]) { + assert(div[name] === undefined, `div.${name} should be undefined`); + } + assert(document.createElementNS(svg, "link").sizes === undefined, "SVG link.sizes"); + assert(document.createElementNS(svg, "output").htmlFor === undefined, "SVG output.htmlFor"); + assert(document.createElementNS(svg, "iframe").sandbox === undefined, "SVG iframe.sandbox"); + assert(document.createElement("svg").relList === undefined, "HTML svg.relList"); + return "ok"; + })() + "#, + ) + .expect("reflected DOMTokenList attributes should evaluate"); + + assert_eq!(result, "ok"); +}