From bb7fdb41bc62d0cd00d8b36eed2fc2fe6b023600 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 14 Oct 2026 05:06:19 +0800 Subject: [PATCH] fix(dom): derive element creation options before validation --- .../constructors/custom_elements_registry.rs | 2 +- moli-renderer-v8/src/custom_elements.rs | 9 +- .../src/custom_elements/element_creation.rs | 21 +-- .../src/custom_elements/registry_runtime.rs | 16 +- .../native_template/detached_bootstrap.rs | 6 - .../src/native_bridge/document.rs | 8 +- .../native_bridge/document/construction.rs | 26 +-- .../document/construction/bridge_callbacks.rs | 19 +-- .../document/construction/elements.rs | 45 ++--- .../document/construction/helpers.rs | 44 ----- .../document/custom_element_options.rs | 159 +++++++++++++++++- .../detached_objects/builders/elements.rs | 12 +- .../detached_objects/method_forwarders.rs | 131 +++++---------- .../document/detached_surface/creation.rs | 3 +- .../creation/document_methods.rs | 94 ++++------- .../tests/element_creation_options.js | 146 ++++++++++++++++ .../src/script_vm/tests/observer_callbacks.rs | 29 ++++ 17 files changed, 448 insertions(+), 322 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/element_creation_options.js diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs b/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs index b9a174b2c1..6ef3106307 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/custom_elements_registry.rs @@ -360,7 +360,7 @@ pub(in crate::context_bootstrap) fn custom_elements_initialize_callback<'s>( scope.throw_exception(exception); return; }; - if !custom_elements::registry_association_matches_document_default( + if !custom_elements::registry_association_matches_document( unsafe { &*host_ptr }, root_document, registry_association, diff --git a/moli-renderer-v8/src/custom_elements.rs b/moli-renderer-v8/src/custom_elements.rs index 506e3b4afa..a3bcaeab48 100644 --- a/moli-renderer-v8/src/custom_elements.rs +++ b/moli-renderer-v8/src/custom_elements.rs @@ -36,9 +36,7 @@ pub(crate) use definition_upgrade::{ upgrade_existing_definition_for_child, upgrade_existing_definition_for_registry, }; mod element_creation; -pub(crate) use element_creation::{ - create_element_for_document_local_name_is_and_registry, is_name_from_create_options_value, -}; +pub(crate) use element_creation::create_element_for_document_local_name_is_and_registry; mod element_state; use element_state::{ definition_name_for_handle, set_dom_custom_element_is_name, set_dom_custom_element_state, @@ -136,9 +134,8 @@ pub(crate) use registry_install::{ }; mod registry_runtime; pub(crate) use registry_runtime::{ - mark_scoped_custom_elements_registry, registry_association_from_create_options_value, - registry_association_from_value, registry_association_matches_document_default, - registry_store_key, + mark_scoped_custom_elements_registry, registry_association_from_value, + registry_association_matches_document, registry_store_key, }; mod registry_initializer; pub(crate) use registry_initializer::initialize_registry_for_subtree; diff --git a/moli-renderer-v8/src/custom_elements/element_creation.rs b/moli-renderer-v8/src/custom_elements/element_creation.rs index d368517734..1827d9cf7d 100644 --- a/moli-renderer-v8/src/custom_elements/element_creation.rs +++ b/moli-renderer-v8/src/custom_elements/element_creation.rs @@ -5,26 +5,7 @@ use super::element_state::{ set_dom_custom_element_state, set_dom_element_prefix, }; use crate::dom::native::CustomElementState; -use crate::{document_runtime::DomHandle, native_bridge::JsContextHost, util::v8str}; - -pub(crate) fn is_name_from_create_options_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option { - if value.is_null_or_undefined() { - return None; - } - if value.is_string() { - return value - .to_string(scope) - .map(|value| value.to_rust_string_lossy(scope)); - } - let options = value.to_object(scope)?; - options - .get(scope, v8str(scope, "is").into()) - .and_then(|value| value.to_string(scope)) - .map(|value| value.to_rust_string_lossy(scope)) -} +use crate::{document_runtime::DomHandle, native_bridge::JsContextHost}; pub(crate) fn create_element_for_document_local_name_is_and_registry<'s>( scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/custom_elements/registry_runtime.rs b/moli-renderer-v8/src/custom_elements/registry_runtime.rs index 087ce8e654..d8a74cff92 100644 --- a/moli-renderer-v8/src/custom_elements/registry_runtime.rs +++ b/moli-renderer-v8/src/custom_elements/registry_runtime.rs @@ -3,7 +3,7 @@ use crate::web_api_interfaces; use crate::{ document_runtime::DomHandle, native_bridge::JsContextHost, - util::{get_private_value, set_private_value, v8str}, + util::{get_private_value, set_private_value}, }; pub(super) const CUSTOM_ELEMENTS_REGISTRY_CHILD_HANDLE_SLOT: &str = @@ -58,19 +58,7 @@ pub(crate) fn registry_association_from_value<'s>( )) } -pub(crate) fn registry_association_from_create_options_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, -) -> Option { - if value.is_null_or_undefined() || value.is_string() { - return None; - } - let options = value.to_object(scope)?; - let registry = options.get(scope, v8str(scope, "customElementRegistry").into())?; - registry_association_from_value(scope, registry) -} - -pub(crate) fn registry_association_matches_document_default( +pub(crate) fn registry_association_matches_document( host: &JsContextHost, document_handle: DomHandle, association: CustomElementRegistryAssociation, diff --git a/moli-renderer-v8/src/native_bridge/bindings/native_template/detached_bootstrap.rs b/moli-renderer-v8/src/native_bridge/bindings/native_template/detached_bootstrap.rs index 80d80c87fc..9e1ec04512 100644 --- a/moli-renderer-v8/src/native_bridge/bindings/native_template/detached_bootstrap.rs +++ b/moli-renderer-v8/src/native_bridge/bindings/native_template/detached_bootstrap.rs @@ -86,12 +86,6 @@ struct NativeBridgeDetachedCreationHelpersDeclaration { )] detached_create_cdata_section: (), - #[webapi( - method = "__detachedCreateElement", - callback = document::bridge_detached_create_element_callback - )] - detached_create_element: (), - #[webapi( method = "__adoptNodeIntoDocument", callback = document::bridge_adopt_node_into_document_callback diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index 1f11638495..9408b5c8b6 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -145,8 +145,11 @@ pub(crate) use css_state::{ node_document_adopted_style_sheets_getter_function, node_document_adopted_style_sheets_setter_function, node_document_style_sheets_getter_function, }; -pub(in crate::native_bridge::document) use custom_element_options::parse_import_node_options; pub(in crate::native_bridge) use custom_element_options::validate_registry_association_for_document; +pub(in crate::native_bridge::document) use custom_element_options::{ + CreateElementOptions, DocumentCreateElementArgs, DocumentCreateElementNsArgs, + parse_import_node_options, +}; pub(crate) use detached_install::detached_iframe_current_content_document_handle; pub(super) use detached_install::install_detached_bridge_methods; pub(in crate::native_bridge) use detached_install::{ @@ -207,6 +210,7 @@ pub(in crate::native_bridge) use detached_objects::{ pub(in crate::native_bridge) use detached_objects::{ detached_shadow_root_active_element_value, detached_shadow_root_selection_value, }; +pub(in crate::native_bridge::document) use detached_surface::create_detached_element_for_document; pub(in crate::native_bridge::document) use detached_surface::set_detached_document_content_type; pub(super) use detached_surface::{ bridge_adopt_node_into_document_callback, bridge_clone_node_into_document_callback, @@ -220,7 +224,7 @@ pub(super) use detached_surface::{ bridge_detached_child_element_count_callback, bridge_detached_child_nodes_callback, bridge_detached_children_callback, bridge_detached_clone_node_callback, bridge_detached_create_cdata_section_callback, bridge_detached_create_comment_callback, - bridge_detached_create_document_fragment_callback, bridge_detached_create_element_callback, + bridge_detached_create_document_fragment_callback, bridge_detached_create_processing_instruction_callback, bridge_detached_create_text_callback, bridge_detached_doctype_name_callback, bridge_detached_doctype_public_id_callback, bridge_detached_doctype_system_id_callback, bridge_detached_document_base_uri_callback, diff --git a/moli-renderer-v8/src/native_bridge/document/construction.rs b/moli-renderer-v8/src/native_bridge/document/construction.rs index a714bbaf5c..d62f77f64d 100644 --- a/moli-renderer-v8/src/native_bridge/document/construction.rs +++ b/moli-renderer-v8/src/native_bridge/document/construction.rs @@ -1,7 +1,4 @@ -use super::super::super::{ - custom_elements, - util::{call_global_bridge_method, v8str}, -}; +use super::super::super::{custom_elements, util::call_global_bridge_method}; use super::super::node::{ node_arg_handle, node_is_document, node_or_foreign_arg_handle_allow_detached, node_runtime_and_handle_from_args, node_runtime_and_handle_from_args_or_detached, @@ -12,8 +9,9 @@ use super::super::{ validate_qualified_element_name_and_namespace, validate_qualified_name_and_namespace, }; use super::{ - XHTML_NS, clone_js_node_like_into_document_object, is_html_document, is_valid_pi_target, - new_attr_object, normalize_namespace, + DocumentCreateElementArgs, DocumentCreateElementNsArgs, XHTML_NS, + clone_js_node_like_into_document_object, is_html_document, is_valid_pi_target, new_attr_object, + normalize_namespace, }; use super::{ detached_adopt_node_method_callback, detached_create_attribute_method_callback, @@ -26,22 +24,6 @@ use super::{ }; use crate::webidl; -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.createElement")] -struct DocumentCreateElementArgs { - #[webidl(required)] - local_name: String, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.createElementNS")] -struct DocumentCreateElementNsArgs { - #[webidl(required, nullable)] - namespace: Option, - #[webidl(required)] - qualified_name: String, -} - #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "Document.createTextNode")] struct DocumentCreateTextNodeArgs { diff --git a/moli-renderer-v8/src/native_bridge/document/construction/bridge_callbacks.rs b/moli-renderer-v8/src/native_bridge/document/construction/bridge_callbacks.rs index 107f5c979a..d036e4b5b8 100644 --- a/moli-renderer-v8/src/native_bridge/document/construction/bridge_callbacks.rs +++ b/moli-renderer-v8/src/native_bridge/document/construction/bridge_callbacks.rs @@ -1,5 +1,5 @@ use super::helpers::{ - create_element_ns_for_document, create_element_options, create_element_wrapper_for_document, + create_element_ns_for_document, create_element_wrapper_for_document, registry_association_for_create_element, registry_association_has_autonomous_definition, validate_create_element_name, validate_create_element_ns_name, }; @@ -22,11 +22,9 @@ pub(in crate::native_bridge) fn bridge_create_element_callback<'s>( rv.set_null(); return; }; - let mut options = create_element_options(scope, &args, 1); - if options.registry_association.is_none() { - options.registry_association = - unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle); - } + let Some(options) = parsed.options.flatten(scope, runtime_ptr, document_handle) else { + return; + }; match create_element_wrapper_for_document( scope, args.this(), @@ -60,11 +58,9 @@ pub(in crate::native_bridge) fn bridge_create_element_ns_callback<'s>( rv.set_null(); return; }; - let mut options = create_element_options(scope, &args, 2); - if options.registry_association.is_none() { - options.registry_association = - unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle); - } + let Some(options) = parsed.options.flatten(scope, runtime_ptr, document_handle) else { + return; + }; if namespace.as_deref() == Some(XHTML_NS) { let (prefix, local_name) = parsed .qualified_name @@ -127,6 +123,7 @@ pub(in crate::native_bridge) fn bridge_create_element_ns_callback<'s>( rv.set_null(); return; }; + options.initialize_non_html_element(runtime_ptr, handle); unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, handle); match unsafe { &mut *runtime_ptr } .native_bridge_mut() diff --git a/moli-renderer-v8/src/native_bridge/document/construction/elements.rs b/moli-renderer-v8/src/native_bridge/document/construction/elements.rs index 92d8ccefde..c8a39b61bb 100644 --- a/moli-renderer-v8/src/native_bridge/document/construction/elements.rs +++ b/moli-renderer-v8/src/native_bridge/document/construction/elements.rs @@ -1,11 +1,10 @@ use super::helpers::{ - create_element_ns_for_document, create_element_options, create_element_with_parts_for_document, + create_element_ns_for_document, create_element_with_parts_for_document, create_element_wrapper_for_document, registry_association_for_create_element, registry_association_has_autonomous_definition, validate_create_element_name, validate_create_element_ns_name, }; use super::*; -use crate::native_bridge::document::validate_registry_association_for_document; use crate::native_bridge::set_wrapped_handle_or_null_for_receiver; pub(in crate::native_bridge) fn node_create_element_callback<'s>( @@ -24,11 +23,11 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>( } return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { - rv.set_null(); + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + rv.set_null(); return; }; if !validate_create_element_name(scope, &parsed.local_name) { @@ -39,19 +38,9 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>( rv.set_null(); return; } - let mut options = create_element_options(scope, &args, 1); - if options.registry_association.is_none() { - options.registry_association = - unsafe { &*runtime_ptr }.custom_element_registry_association(handle); - } - if !validate_registry_association_for_document( - scope, - runtime_ptr, - handle, - options.registry_association, - ) { + let Some(options) = parsed.options.flatten(scope, runtime_ptr, handle) else { return; - } + }; if !is_html_document(runtime, handle) { let namespace = runtime .dom_host() @@ -68,6 +57,7 @@ pub(in crate::native_bridge) fn node_create_element_callback<'s>( rv.set_null(); return; }; + options.initialize_non_html_element(runtime_ptr, created_handle); unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, created_handle); set_wrapped_handle_or_null_for_receiver( scope, @@ -109,30 +99,20 @@ pub(in crate::native_bridge) fn node_create_element_ns_callback<'s>( } return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { - rv.set_null(); + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + rv.set_null(); return; }; let namespace = normalize_namespace(parsed.namespace); if !validate_create_element_ns_name(scope, namespace.as_deref(), &parsed.qualified_name) { return; } - let mut options = create_element_options(scope, &args, 2); - if options.registry_association.is_none() { - options.registry_association = - unsafe { &*runtime_ptr }.custom_element_registry_association(handle); - } - if !validate_registry_association_for_document( - scope, - runtime_ptr, - handle, - options.registry_association, - ) { + let Some(options) = parsed.options.flatten(scope, runtime_ptr, handle) else { return; - } + }; if namespace.as_deref() == Some(XHTML_NS) { let (prefix, local_name) = parsed .qualified_name @@ -195,6 +175,7 @@ pub(in crate::native_bridge) fn node_create_element_ns_callback<'s>( rv.set_null(); return; }; + options.initialize_non_html_element(runtime_ptr, created_handle); unsafe { &mut *runtime_ptr }.capture_node_creation_stack_trace(scope, created_handle); set_wrapped_handle_or_null_for_receiver( scope, diff --git a/moli-renderer-v8/src/native_bridge/document/construction/helpers.rs b/moli-renderer-v8/src/native_bridge/document/construction/helpers.rs index de59843996..5277b01470 100644 --- a/moli-renderer-v8/src/native_bridge/document/construction/helpers.rs +++ b/moli-renderer-v8/src/native_bridge/document/construction/helpers.rs @@ -85,11 +85,6 @@ pub(super) fn registry_association_has_autonomous_definition( } } -pub(super) struct CreateElementOptions { - pub(super) is_name: Option, - pub(super) registry_association: Option, -} - pub(super) fn validate_create_element_name( scope: &mut v8::PinScope<'_, '_>, local_name: &str, @@ -119,42 +114,3 @@ pub(super) fn validate_create_element_ns_name( } } } - -pub(super) fn create_element_options( - scope: &mut v8::PinScope<'_, '_>, - args: &v8::FunctionCallbackArguments<'_>, - index: i32, -) -> CreateElementOptions { - let value = args.get(index); - if value.is_null_or_undefined() { - return CreateElementOptions { - is_name: None, - registry_association: None, - }; - } - if value.is_string() { - // Legacy string createElement/createElementNS options are ignored by - // current DOM custom-elements semantics; only dictionary `is` is used. - return CreateElementOptions { - is_name: None, - registry_association: None, - }; - } - let Some(options) = value.to_object(scope) else { - return CreateElementOptions { - is_name: None, - registry_association: None, - }; - }; - let is_name = options - .get(scope, v8str(scope, "is").into()) - .and_then(|value| value.to_string(scope)) - .map(|value| value.to_rust_string_lossy(scope)); - let registry_association = options - .get(scope, v8str(scope, "customElementRegistry").into()) - .and_then(|value| custom_elements::registry_association_from_value(scope, value)); - CreateElementOptions { - is_name, - registry_association, - } -} diff --git a/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs b/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs index 0a1fea83fa..c7e39e4207 100644 --- a/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs +++ b/moli-renderer-v8/src/native_bridge/document/custom_element_options.rs @@ -2,6 +2,163 @@ use crate::custom_elements; use crate::document_runtime::DomHandle; use crate::native_bridge::{JsContextHost, throw_dom_exception}; use crate::util::{throw_type_error, v8str}; +use crate::web_api_interfaces; +use crate::webidl; + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "Document.createElement")] +pub(in crate::native_bridge::document) struct DocumentCreateElementArgs { + #[webidl(required)] + pub(in crate::native_bridge::document) local_name: String, + #[webidl(with = create_element_options_argument)] + pub(in crate::native_bridge::document) options: CreateElementOptions, +} + +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "Document.createElementNS")] +pub(in crate::native_bridge::document) struct DocumentCreateElementNsArgs { + #[webidl(required, nullable)] + pub(in crate::native_bridge::document) namespace: Option, + #[webidl(required)] + pub(in crate::native_bridge::document) qualified_name: String, + #[webidl(with = create_element_options_argument)] + pub(in crate::native_bridge::document) options: CreateElementOptions, +} + +#[derive(Default, webidl::WebIdlDictionary)] +#[webidl(prefix = "ElementCreationOptions")] +pub(in crate::native_bridge::document) struct CreateElementOptions { + // An omitted nullable member and an explicitly null registry have different + // meanings. Keep its presence while reusing native interface conversion. + #[webidl(name = "customElementRegistry", with = nullable_creation_registry)] + pub(in crate::native_bridge::document) registry_association: + Option, + #[webidl(name = "is")] + pub(in crate::native_bridge::document) is_name: Option, +} + +impl<'s> webidl::WebIdlConverter<'s> for CreateElementOptions { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: webidl::Context, + _options: &(), + ) -> Result { + // Objects select the dictionary even if they are boxed strings or have + // a toString method. Nullish values select the empty dictionary. + if value.is_object() || value.is_null_or_undefined() { + return webidl::parse_dictionary::(scope, value, context) + .map(Option::unwrap_or_default); + } + // Other primitives select the legacy DOMString member, which the DOM + // algorithm ignores. Conversion still rejects Symbols. + webidl::convert::(scope, value, context)?; + Ok(Self::default()) + } +} + +fn create_element_options_argument<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, +) -> Result { + let prefix = if index == 1 { + "Document.createElement" + } else { + "Document.createElementNS" + }; + webidl::argument( + scope, + args, + index, + webidl::Context::argument(prefix, index as usize + 1), + ) +} + +fn nullable_creation_registry<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + member: &'static str, +) -> Result, webidl::WebIdlError> { + let context = webidl::Context::member("ElementCreationOptions", member); + let Some(value) = webidl::property_result(scope, object, member, context)? else { + return Ok(None); + }; + if value.is_undefined() { + return Ok(None); + } + if value.is_null() { + return Ok(Some( + custom_elements::CustomElementRegistryAssociation::Null, + )); + } + let registry = webidl::convert_with_options::( + scope, + value, + context, + &webidl::InterfaceOptions { + name: web_api_interfaces::CustomElementRegistry::NAME, + brand_check: web_api_interfaces::CustomElementRegistry::is_instance, + }, + )?; + let target = moli_webapi_declare::web_api_object_target(scope, registry.0) + .expect("converted CustomElementRegistry has native identity"); + Ok(Some( + custom_elements::CustomElementRegistryAssociation::Registry( + custom_elements::registry_store_key(scope, target), + ), + )) +} + +impl CreateElementOptions { + pub(in crate::native_bridge::document) fn initialize_non_html_element( + &self, + runtime_ptr: *mut JsContextHost, + handle: DomHandle, + ) { + let runtime = unsafe { &mut *runtime_ptr }; + if let Some(registry) = self.registry_association { + runtime.set_custom_element_registry_association(handle, registry); + } + if let Some(is_name) = &self.is_name { + runtime + .dom_host_mut() + .set_custom_element_is_name(handle, Some(is_name.clone())); + } + } + + pub(in crate::native_bridge::document) fn flatten( + mut self, + scope: &mut v8::PinScope<'_, '_>, + runtime_ptr: *mut JsContextHost, + document_handle: DomHandle, + ) -> Option { + if self.registry_association.is_some() && self.is_name.is_some() { + throw_dom_exception( + scope, + "NotSupportedError", + 9, + "An explicit CustomElementRegistry cannot be combined with is.", + ); + return None; + } + if !validate_registry_association_for_document( + scope, + runtime_ptr, + document_handle, + self.registry_association, + ) { + return None; + } + if self.registry_association.is_none() { + self.registry_association = + unsafe { &*runtime_ptr }.custom_element_registry_association(document_handle); + } + Some(self) + } +} pub(in crate::native_bridge) fn validate_registry_association_for_document( scope: &mut v8::PinScope<'_, '_>, @@ -12,7 +169,7 @@ pub(in crate::native_bridge) fn validate_registry_association_for_document( let Some(registry_association) = registry_association else { return true; }; - if custom_elements::registry_association_matches_document_default( + if custom_elements::registry_association_matches_document( unsafe { &*runtime_ptr }, document_handle, registry_association, diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs index a2da557b01..d3c3b70ac3 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/elements.rs @@ -544,9 +544,6 @@ pub(in crate::native_bridge::document) fn build_detached_element_object<'s>( v8_string(scope, document_kind)?.into(), ); let attributes = new_map_object(scope); - if let Some(is_name) = is_name { - detached_map_set(scope, attributes, "is", is_name); - } let _ = state.set(scope, v8str(scope, "attributes").into(), attributes.into()); let namespace_attributes = new_map_object(scope); let _ = state.set( @@ -573,10 +570,13 @@ pub(in crate::native_bridge::document) fn build_detached_element_object<'s>( ); if let Some(is_name) = is_name { let runtime = unsafe { &mut *runtime_ptr }; - let _ = runtime.dom_host_mut().set_attribute(handle, "is", is_name); - if runtime + let _ = runtime .dom_host_mut() - .set_custom_element_state(handle, CustomElementState::Undefined) + .set_custom_element_is_name(handle, Some(is_name.to_owned())); + if namespace_uri.as_deref() == Some(XHTML_NS) + && runtime + .dom_host_mut() + .set_custom_element_state(handle, CustomElementState::Undefined) { runtime.note_style_subtree_context_change(handle); } diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs index 1a15f62770..8171b45d6c 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs @@ -1,6 +1,9 @@ use super::*; use crate::detached_event_target::dispatch_detached_focus_event; use crate::dom_parser::map_live_value_to_foreign; +use crate::native_bridge::document::{ + DocumentCreateElementArgs, DocumentCreateElementNsArgs, create_detached_element_for_document, +}; use crate::util::{ context_host_ptr_from_global_bridge, get_private_object, get_private_value, set_private_value, }; @@ -35,22 +38,6 @@ struct DetachedDomImplementationDeclaration<'scope> { owner_document: v8::Local<'scope, v8::Object>, } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.createElement")] -struct DetachedForwardCreateElementArgs { - #[webidl(required)] - local_name: String, -} - -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.createElementNS")] -struct DetachedForwardCreateElementNsArgs { - #[webidl(required, nullable)] - namespace: Option, - #[webidl(required)] - qualified_name: String, -} - #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "Document.createCDATASection")] struct DetachedForwardCreateCdataSectionArgs { @@ -769,26 +756,19 @@ pub(in crate::native_bridge::document) fn detached_create_html_element_method_ca args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(local_name) = v8_string(scope, &parsed.local_name) else { - rv.set_null(); - return; - }; - match call_global_bridge_method( + match create_detached_element_for_document( scope, - "__detachedCreateElement", - &[ - args.this().into(), - local_name.into(), - v8str(scope, XHTML_NS).into(), - v8str(scope, "html").into(), - v8str(scope, "name").into(), - args.get(1), - ], + args.this(), + &parsed.local_name, + Some(XHTML_NS.to_owned()), + "html", + false, + parsed.options, ) { - Some(value) => rv.set(value), + Some(value) => rv.set(value.into()), None => rv.set_null(), } } @@ -798,26 +778,19 @@ pub(in crate::native_bridge::document) fn detached_create_xml_element_method_cal args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(local_name) = v8_string(scope, &parsed.local_name) else { - rv.set_null(); - return; - }; - match call_global_bridge_method( + match create_detached_element_for_document( scope, - "__detachedCreateElement", - &[ - args.this().into(), - local_name.into(), - v8::null(scope).into(), - v8str(scope, "xml").into(), - v8str(scope, "name").into(), - args.get(1), - ], + args.this(), + &parsed.local_name, + None, + "xml", + false, + parsed.options, ) { - Some(value) => rv.set(value), + Some(value) => rv.set(value.into()), None => rv.set_null(), } } @@ -827,32 +800,19 @@ pub(in crate::native_bridge::document) fn detached_create_html_element_ns_method args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) - else { + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else { - rv.set_null(); - return; - }; - let namespace = parsed - .namespace - .as_deref() - .and_then(|namespace| v8_string(scope, namespace).map(Into::into)) - .unwrap_or_else(|| v8::null(scope).into()); - match call_global_bridge_method( + match create_detached_element_for_document( scope, - "__detachedCreateElement", - &[ - args.this().into(), - qualified_name.into(), - namespace, - v8str(scope, "html").into(), - v8str(scope, "qualified").into(), - args.get(2), - ], + args.this(), + &parsed.qualified_name, + parsed.namespace, + "html", + true, + parsed.options, ) { - Some(value) => rv.set(value), + Some(value) => rv.set(value.into()), None => rv.set_null(), } } @@ -862,32 +822,19 @@ pub(in crate::native_bridge::document) fn detached_create_xml_element_ns_method_ args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) - else { + let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else { - rv.set_null(); - return; - }; - let namespace = parsed - .namespace - .as_deref() - .and_then(|namespace| v8_string(scope, namespace).map(Into::into)) - .unwrap_or_else(|| v8::null(scope).into()); - match call_global_bridge_method( + match create_detached_element_for_document( scope, - "__detachedCreateElement", - &[ - args.this().into(), - qualified_name.into(), - namespace, - v8str(scope, "xml").into(), - v8str(scope, "qualified").into(), - args.get(2), - ], + args.this(), + &parsed.qualified_name, + parsed.namespace, + "xml", + true, + parsed.options, ) { - Some(value) => rv.set(value), + Some(value) => rv.set(value.into()), None => rv.set_null(), } } diff --git a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation.rs b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation.rs index e46b11bdfc..7fa9ff8e21 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation.rs @@ -7,10 +7,11 @@ pub(in crate::native_bridge) use self::clone_adopt::{ bridge_adopt_node_into_document_callback, bridge_clone_node_into_document_callback, bridge_detached_clone_node_callback, }; +pub(in crate::native_bridge::document) use self::document_methods::create_detached_element_for_document; pub(in crate::native_bridge) use self::document_methods::{ bridge_create_cdata_section_not_supported_callback, bridge_detached_create_cdata_section_callback, bridge_detached_create_comment_callback, - bridge_detached_create_document_fragment_callback, bridge_detached_create_element_callback, + bridge_detached_create_document_fragment_callback, bridge_detached_create_processing_instruction_callback, bridge_detached_create_text_callback, }; pub(in crate::native_bridge) use self::dom_implementation::{ diff --git a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/document_methods.rs b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/document_methods.rs index 53c1508a4c..03d564e486 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/document_methods.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/document_methods.rs @@ -1,6 +1,5 @@ use super::super::*; -use crate::custom_elements; -use crate::native_bridge::document::validate_registry_association_for_document; +use crate::native_bridge::document::CreateElementOptions; use crate::webidl; #[derive(webidl::WebIdlArgs)] @@ -40,19 +39,6 @@ struct DetachedDocumentCreateCdataSectionArgs { data: String, } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "Document.createElement")] -struct DetachedDocumentCreateElementBridgeArgs { - #[webidl(index = 1, required)] - qualified_name: String, - #[webidl(index = 2, required, nullable)] - namespace_uri: Option, - #[webidl(index = 3, required)] - document_kind: String, - #[webidl(index = 4)] - namespace_validation: Option, -} - pub(in crate::native_bridge) fn bridge_detached_create_text_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -193,29 +179,20 @@ pub(in crate::native_bridge) fn bridge_create_cdata_section_not_supported_callba ); } -pub(in crate::native_bridge) fn bridge_detached_create_element_callback<'s>( +pub(in crate::native_bridge::document) fn create_detached_element_for_document<'s>( scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let Ok(document) = v8::Local::::try_from(args.get(0)) else { - rv.set_null(); - return; - }; - let Some(parsed) = webidl::parse_args::(scope, &args) - else { - return; - }; - let mut namespace_uri = normalize_namespace(parsed.namespace_uri); - let document_kind = parsed.document_kind; - let validation = parsed.namespace_validation.as_deref(); - let validation_error = if validation == Some("qualified") { - validate_qualified_element_name_and_namespace( - namespace_uri.as_deref(), - &parsed.qualified_name, - ) - .err() - } else if !validate_element_name(&parsed.qualified_name) { + document: v8::Local<'s, v8::Object>, + qualified_name: &str, + namespace_uri: Option, + document_kind: &str, + split_qualified_name: bool, + options: CreateElementOptions, +) -> Option> { + let mut namespace_uri = normalize_namespace(namespace_uri); + let validation_error = if split_qualified_name { + validate_qualified_element_name_and_namespace(namespace_uri.as_deref(), qualified_name) + .err() + } else if !validate_element_name(qualified_name) { Some(( "InvalidCharacterError", 5, @@ -226,42 +203,31 @@ pub(in crate::native_bridge) fn bridge_detached_create_element_callback<'s>( }; if let Some((name, code, message)) = validation_error { throw_dom_exception(scope, name, code, message); - return; + return None; } + let (runtime_ptr, document_handle) = + node_runtime_and_handle_from_object_or_detached(scope, document).ok()?; + let options = options.flatten(scope, runtime_ptr, document_handle)?; + // Conversion and DOM exceptions use the binding's realm. Allocation uses + // the Document's realm so native ownership does not depend on the caller. + let context = document.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); if namespace_uri.is_none() - && validation != Some("qualified") + && !split_qualified_name && document_kind == "xml" && detached_document_content_type_value(scope, document).as_deref() == Some("application/xhtml+xml") { namespace_uri = Some(XHTML_NS.to_owned()); } - let registry_association = - custom_elements::registry_association_from_create_options_value(scope, args.get(5)); - let is_name = custom_elements::is_name_from_create_options_value(scope, args.get(5)); - if let Some(registry_association) = registry_association - && let Some(runtime_ptr) = crate::util::context_host_ptr_from_global_bridge(scope) - && let Some(document_handle) = detached_native_handle(scope, document) - && !validate_registry_association_for_document( - scope, - runtime_ptr, - document_handle, - Some(registry_association), - ) - { - return; - } - match build_detached_element_object( + build_detached_element_object( scope, document, - &parsed.qualified_name, + qualified_name, namespace_uri, - &document_kind, - validation == Some("qualified"), - is_name.as_deref(), - registry_association, - ) { - Some(element) => rv.set(element.into()), - None => rv.set_null(), - } + document_kind, + split_qualified_name, + options.is_name.as_deref(), + options.registry_association, + ) } diff --git a/moli-renderer-v8/src/script_vm/tests/element_creation_options.js b/moli-renderer-v8/src/script_vm/tests/element_creation_options.js new file mode 100644 index 0000000000..aaf3b45c92 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/element_creation_options.js @@ -0,0 +1,146 @@ +(async () => { + const checks = []; + const observe = action => {try {return action();} catch (error) {return {exception: error.name, message: String(error)};}}; + const check = (name, actual, expected) => checks.push({name, actual, expected, passed: JSON.stringify(actual) === JSON.stringify(expected)}); + const capture = (action, sentinel, callee) => { + try {action(); return null;} catch (error) { + if (error === sentinel) return 'sentinel'; + if (callee && error.name === 'TypeError' && Object.getPrototypeOf(error) !== callee.TypeError.prototype) return 'wrong-realm-TypeError'; + return error.name; + } + }; + const htmlNS = 'http://www.w3.org/1999/xhtml'; + const frame = document.createElement('iframe'); document.body.appendChild(frame); + let popup; + try { + const realms = [['main', window], ['iframe', frame.contentWindow]]; + if (globalThis.__elementOptionsIncludePopup !== false) { + popup = window.open('', '_blank'); + if (!popup) throw new Error('popup unavailable'); + await new Promise(resolve => setTimeout(resolve, 0)); + realms.push(['popup', popup]); + } + for (const [ownerName, owner] of realms) { + const docs = [ + ['live', owner.document], + ['html', owner.document.implementation.createHTMLDocument('')], + ['xml', owner.document.implementation.createDocument(null, 'root')], + ['parsed-html', new owner.DOMParser().parseFromString('', 'text/html')], + ['parsed-xml', new owner.DOMParser().parseFromString('', 'application/xml')], + ['new-document', new owner.Document()], + ]; + const scoped = new owner.CustomElementRegistry(); + for (const [docName, doc] of docs) { + for (const [calleeName, callee] of realms) { + for (const ns of [false, true]) { + const prefix = `${ownerName}/${docName}/${calleeName}/${ns ? 'createElementNS' : 'createElement'}`; + const method = callee.Document.prototype[ns ? 'createElementNS' : 'createElement']; + const create = (tag, options, namespace = htmlNS) => ns ? method.call(doc, namespace, tag, options) : method.call(doc, tag, options); + for (const valid of [false, true]) { + for (const stop of [-1, 0, 1, 2, 3, 4]) { + const log = [], sentinel = {}; + const argument = (text, label, position) => ({toString() {log.push(label); if (stop === position) throw sentinel; return text;}}); + const options = { + get customElementRegistry() {log.push('registry'); if (stop === 2) throw sentinel; return undefined;}, + get is() {log.push('is'); if (stop === 3) throw sentinel; return argument('x-order', 'is-string', 4);}, + }; + const tag = argument(valid ? 'div' : 'a b', 'name', 1); + const namespace = argument(htmlNS, 'namespace', 0); + const labels = ns ? ['namespace', 'name', 'registry', 'is', 'is-string'] : ['name', 'registry', 'is', 'is-string']; + const stopLabel = ['namespace', 'name', 'registry', 'is', 'is-string'][stop]; + const stopIndex = labels.indexOf(stopLabel); + const expectedLog = stopIndex < 0 ? labels : labels.slice(0, stopIndex + 1); + check(prefix + `/order-${valid}-${stop}`, [capture(() => create(tag, options, namespace), sentinel, callee), log], [stopIndex < 0 ? (valid ? null : 'InvalidCharacterError') : 'sentinel', expectedLog]); + } + } + const plain = observe(() => create('button', undefined).outerHTML); + for (const value of [undefined, null, 'x-ignored', 0, 1, false, true, 1n]) { + check(prefix + `/legacy-${typeof value}-${String(value)}`, observe(() => create('button', value).outerHTML), plain); + } + for (const valid of [false, true]) check(prefix + `/symbol-${valid}`, capture(() => create(valid ? 'div' : 'a b', Symbol()), undefined, callee), 'TypeError'); + for (const value of [{}, {is: undefined}, {customElementRegistry: undefined, is: undefined}]) { + check(prefix + `/missing-is-${checks.length}`, observe(() => [create('button', value).outerHTML, create('button', value).getAttribute('is')]), [plain, null]); + } + for (const value of [null, '', 'x-built-in', 1, false]) { + const log = [], options = {get customElementRegistry() {log.push('registry'); return undefined;}, get is() {log.push('is'); return value;}}; + check(prefix + `/is-${String(value)}`, observe(() => [create('button', options).getAttribute('is'), log]), [null, ['registry', 'is']]); + } + for (const make of [() => new owner.String('x-ignored'), () => function options() {}, () => Object.create({is: 'x-inherited'})]) { + const options = make(), log = []; + Object.defineProperty(options, 'customElementRegistry', {get() {log.push('registry'); return undefined;}}); + Object.defineProperty(options, 'is', {get() {log.push('is'); return 'x-boxed';}}); + Object.defineProperty(options, 'toString', {value() {throw new Error('dictionary toString must not run');}}); + check(prefix + `/dictionary-object-${checks.length}`, [capture(() => create('button', options), undefined, callee), log], [null, ['registry', 'is']]); + } + for (const registry of [null, scoped]) { + for (const value of [null, '', 'x-built-in']) { + for (const valid of [false, true]) { + const log = [], options = {get customElementRegistry() {log.push('registry'); return registry;}, get is() {log.push('is'); return value;}}; + check(prefix + `/registry-is-${checks.length}`, [capture(() => create(valid ? 'div' : 'a b', options), undefined, callee), log], [valid ? 'NotSupportedError' : 'InvalidCharacterError', ['registry', 'is']]); + } + } + check(prefix + `/registry-identity-${checks.length}`, observe(() => create('div', {customElementRegistry: registry, is: undefined}).customElementRegistry === registry), true); + } + const revoked = owner.Proxy.revocable(scoped, {}); revoked.revoke(); + let traps = 0; + const invalidRegistries = [{}, Object.create(scoped), Object.create(owner.CustomElementRegistry.prototype), new owner.Proxy(scoped, {get() {traps++; throw 42;}, getPrototypeOf() {traps++; throw 42;}}), revoked.proxy, false, 1, 'registry', Symbol()]; + for (const registry of invalidRegistries) { + const log = [], options = {get customElementRegistry() {log.push('registry'); return registry;}, get is() {log.push('unexpected'); return 'x-built-in';}}; + check(prefix + `/invalid-registry-${checks.length}`, [capture(() => create('a b', options), undefined, callee), log, traps], ['TypeError', ['registry'], 0]); + } + const optionsProxy = new owner.Proxy({}, {get(target, key) {return key === 'customElementRegistry' ? undefined : key === 'is' ? 'x-proxy-options' : Reflect.get(target, key);}}); + check(prefix + '/options-author-proxy', capture(() => create('div', optionsProxy), undefined, callee), null); + const revokedOptions = owner.Proxy.revocable({}, {}); revokedOptions.revoke(); + check(prefix + '/options-revoked-proxy', capture(() => create('a b', revokedOptions.proxy), undefined, callee), 'TypeError'); + for (let count = 0; count < (ns ? 2 : 1); count++) { + let conversions = 0; + const args = Array.from({length: count}, () => ({toString() {conversions++; throw 42;}})); + check(prefix + `/arity-${count}`, [capture(() => method.call(doc, ...args), undefined, callee), conversions], ['TypeError', 0]); + } + const receivers = [{}, Object.create(doc), new owner.Proxy(doc, {})]; + const revokedDocument = owner.Proxy.revocable(doc, {}); revokedDocument.revoke(); receivers.push(revokedDocument.proxy); + for (const receiver of receivers) { + let conversions = 0; + const value = {toString() {conversions++; throw 42;}}; + const args = ns ? [value, value, value] : [value, value]; + check(prefix + `/receiver-${checks.length}`, [capture(() => method.call(receiver, ...args), undefined, callee), conversions], ['TypeError', 0]); + } + if (ns) { + const log = [], sentinel = {}; + check(prefix + '/namespace-before-flatten', [capture(() => create('p:div', {customElementRegistry: null, is: 'x-built-in'}, null), undefined, callee)], ['NamespaceError']); + const options = {get customElementRegistry() {log.push('registry'); return undefined;}, get is() {log.push('is'); throw sentinel;}}; + check(prefix + '/options-before-namespace', [capture(() => create('p:div', options, null), sentinel, callee), log], ['sentinel', ['registry', 'is']]); + } + } + } + } + } + for (const [name, w] of realms) { + for (const ns of [false, true]) { + const label = `${name}/definition-reentrancy/${ns}`; + const registry = new w.CustomElementRegistry(); + let constructed = 0; + class Created extends w.HTMLElement {constructor() {super(); constructed++;}} + const options = {customElementRegistry: registry, get is() {registry.define('x-options-' + ns, Created); return undefined;}}; + check(label, observe(() => { + const element = ns ? w.document.createElementNS(htmlNS, 'x-options-' + ns, options) : w.document.createElement('x-options-' + ns, options); + return [element instanceof Created, element.ownerDocument === w.document, element.customElementRegistry === registry, constructed]; + }), [true, true, true, 1]); + const sentinel = {}; + const badOptions = {customElementRegistry: registry, get is() {throw sentinel;}}; + check(label + '/no-construction-after-throw', [capture(() => ns ? w.document.createElementNS(htmlNS, 'x-options-' + ns, badOptions) : w.document.createElement('x-options-' + ns, badOptions), sentinel), constructed], ['sentinel', 1]); + } + } + // Genuine cross-realm registries within one Page share the native host. + const foreign = new frame.contentWindow.CustomElementRegistry(); + for (const ns of [false, true]) { + check(`cross-realm-registry/${ns}`, observe(() => { + const element = ns ? document.createElementNS(null, 'foreign', {customElementRegistry: foreign}) : document.createElement('foreign', {customElementRegistry: foreign}); + return element.customElementRegistry === foreign; + }), true); + } + } catch (error) {checks.push({name: 'uncaught', actual: String(error), expected: 'no exception', passed: false});} + finally { frame.remove(); if (popup) popup.close(); } + globalThis.__uiEventResults = {complete: true, includePopup: Boolean(popup), total: checks.length, passed: checks.filter(c => c.passed).length, checks}; + return checks.every(c => c.passed); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/observer_callbacks.rs b/moli-renderer-v8/src/script_vm/tests/observer_callbacks.rs index 435cae282c..e63a9ca135 100644 --- a/moli-renderer-v8/src/script_vm/tests/observer_callbacks.rs +++ b/moli-renderer-v8/src/script_vm/tests/observer_callbacks.rs @@ -520,3 +520,32 @@ async fn dom_implementation_derives_receiver_and_argument_conversion_in_each_doc assert_eq!(vm.eval("__uiEventResults.includePopup").unwrap(), "false"); assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "4344"); } + +#[tokio::test(flavor = "current_thread")] +async fn element_creation_options_convert_before_dom_validation_in_each_document_realm() { + let loader = static_http_loader(std::iter::empty::()); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://element-creation-options.test/", + &loader, + ); + // Real popup Pages run the same fixture over CDP. + vm.exec("globalThis.__elementOptionsIncludePopup = false;", None) + .unwrap(); + vm.exec(include_str!("element_creation_options.js"), None) + .unwrap(); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(globalThis.__uiEventResults?.complete === true)", + "true", + "element creation options conversion", + ) + .await; + assert_eq!( + vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))") + .unwrap(), + "[]", + ); + assert_eq!(vm.eval("__uiEventResults.includePopup").unwrap(), "false"); + assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "3106"); +}