fix(webidl): allocate factory results in relevant realms

This commit is contained in:
ldm0
2026-09-13 16:57:17 +08:00
parent 2054fd7248
commit 0f88092566
12 changed files with 381 additions and 125 deletions
@@ -1897,10 +1897,13 @@ pub(crate) fn canvas_context_create_image_data_callback<'s>(
rv.set(v8::undefined(scope).into());
return;
};
if let Some(image_data) = build_image_data_object(scope, width, height) {
let relevant_context = canvas_context_relevant_context(scope, args.this())
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
if let Some(image_data) = build_image_data_object(target_scope, width, height) {
rv.set(image_data.into());
} else {
rv.set(v8::undefined(scope).into());
rv.set(v8::undefined(target_scope).into());
}
}
@@ -2010,10 +2013,13 @@ pub(crate) fn canvas_context_get_image_data_callback<'s>(
} else {
blank_image_data(width, height)
};
let Some(image_data) = build_image_data_object_with_bytes(scope, width, height, bytes)
.or_else(|| build_image_data_object(scope, width, height))
else {
rv.set(v8::undefined(scope).into());
let relevant_context = canvas_context_relevant_context(scope, args.this())
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let image_data = build_image_data_object_with_bytes(target_scope, width, height, bytes)
.or_else(|| build_image_data_object(target_scope, width, height));
let Some(image_data) = image_data else {
rv.set(v8::undefined(target_scope).into());
return;
};
rv.set(image_data.into());
@@ -106,6 +106,34 @@ fn current_document_object<'s>(
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
fn dom_implementation_owner_document<'s>(
scope: &mut v8::PinScope<'s, '_>,
implementation: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
get_private_object(
scope,
implementation,
DOM_IMPLEMENTATION_OWNER_DOCUMENT_SLOT,
)
.or_else(|| current_document_object(scope))
}
fn create_document_type_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
parsed: &DomImplementationCreateDocumentTypeArgs,
owner_document: Option<v8::Local<'s, v8::Object>>,
) -> Option<v8::Local<'s, v8::Value>> {
let qualified_name = v8_string(scope, &parsed.qualified_name)?;
let public_id = v8_string(scope, &parsed.public_id)?;
let system_id = v8_string(scope, &parsed.system_id)?;
let mut argv = vec![qualified_name.into(), public_id.into(), system_id.into()];
if let Some(owner_document) = owner_document {
argv.push(owner_document.into());
}
let (bridge, method) = global_bridge_method(scope, "__createDetachedDocumentType")?;
method.call(scope, bridge.into(), &argv)
}
fn dom_implementation_create_document_type_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -124,30 +152,13 @@ fn dom_implementation_create_document_type_callback<'s>(
);
return;
}
let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else {
rv.set_undefined();
return;
};
let Some(public_id) = v8_string(scope, &parsed.public_id) else {
rv.set_undefined();
return;
};
let Some(system_id) = v8_string(scope, &parsed.system_id) else {
rv.set_undefined();
return;
};
let mut argv = vec![qualified_name.into(), public_id.into(), system_id.into()];
if let Some(owner_document) =
get_private_object(scope, args.this(), DOM_IMPLEMENTATION_OWNER_DOCUMENT_SLOT)
.or_else(|| current_document_object(scope))
{
argv.push(owner_document.into());
}
let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedDocumentType") else {
rv.set_undefined();
return;
};
match method.call(scope, bridge.into(), &argv) {
let owner_document = dom_implementation_owner_document(scope, args.this());
let relevant_context = owner_document
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
.or_else(|| args.this().get_creation_context(scope))
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match create_document_type_value(target_scope, &parsed, owner_document) {
Some(value) => rv.set(value),
None => rv.set_undefined(),
}
@@ -168,39 +179,48 @@ fn dom_implementation_create_html_document_callback<'s>(
else {
return;
};
let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedHTMLDocument") else {
rv.set_undefined();
return;
};
let call_args = match parsed.title {
Some(title) => {
let Some(title) = v8_string(scope, &title) else {
rv.set_undefined();
return;
};
vec![title.into()]
}
None => Vec::new(),
};
match method.call(scope, bridge.into(), &call_args) {
let owner_document = dom_implementation_owner_document(scope, args.this());
let relevant_context = owner_document
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
.or_else(|| args.this().get_creation_context(scope))
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match create_html_document_value(target_scope, parsed.title.as_deref()) {
Some(value) => rv.set(value),
None => rv.set_undefined(),
}
}
fn create_html_document_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
title: Option<&str>,
) -> Option<v8::Local<'s, v8::Value>> {
let (bridge, method) = global_bridge_method(scope, "__createDetachedHTMLDocument")?;
let call_args = match title {
Some(title) => vec![v8_string(scope, title)?.into()],
None => Vec::new(),
};
method.call(scope, bridge.into(), &call_args)
}
enum DomImplementationQualifiedName {
Null,
String(String),
}
fn dom_implementation_create_document_qualified_name<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<v8::Local<'s, v8::Value>> {
) -> Option<DomImplementationQualifiedName> {
if value.is_null() {
return Some(v8::null(scope).into());
return Some(DomImplementationQualifiedName::Null);
}
match webidl::convert::<webidl::DomString>(
scope,
value,
webidl::Context::argument("DOMImplementation.createDocument", 2),
) {
Ok(value) => v8_string(scope, &value.0).map(v8::Local::<v8::Value>::from),
Ok(value) => Some(DomImplementationQualifiedName::String(value.0)),
Err(error) => {
webidl::throw_error(scope, &error);
None
@@ -208,6 +228,28 @@ fn dom_implementation_create_document_qualified_name<'s>(
}
}
fn create_document_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
namespace_uri: Option<&str>,
qualified_name: &DomImplementationQualifiedName,
doctype: Option<v8::Local<'s, v8::Object>>,
) -> Option<v8::Local<'s, v8::Value>> {
let namespace_uri = match namespace_uri {
Some(namespace_uri) => v8_string(scope, namespace_uri)?.into(),
None => v8::null(scope).into(),
};
let qualified_name = match qualified_name {
DomImplementationQualifiedName::Null => v8::null(scope).into(),
DomImplementationQualifiedName::String(value) => v8_string(scope, value)?.into(),
};
let doctype = doctype
.map(v8::Local::<v8::Value>::from)
.unwrap_or_else(|| v8::null(scope).into());
let argv = [namespace_uri, qualified_name, doctype];
let (bridge, method) = global_bridge_method(scope, "__createDetachedXmlDocument")?;
method.call(scope, bridge.into(), &argv)
}
fn dom_implementation_create_document_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -217,32 +259,24 @@ fn dom_implementation_create_document_callback<'s>(
else {
return;
};
let namespace_uri = match parsed.namespace_uri {
Some(namespace_uri) => {
let Some(namespace_uri) = v8_string(scope, &namespace_uri) else {
rv.set_undefined();
return;
};
namespace_uri.into()
}
None => v8::null(scope).into(),
};
let Some(qualified_name) =
dom_implementation_create_document_qualified_name(scope, parsed.qualified_name)
else {
rv.set_undefined();
return;
};
let doctype = parsed
.doctype
.map(v8::Local::<v8::Value>::from)
.unwrap_or_else(|| v8::null(scope).into());
let argv = [namespace_uri, qualified_name, doctype];
let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedXmlDocument") else {
rv.set_undefined();
return;
};
match method.call(scope, bridge.into(), &argv) {
let owner_document = dom_implementation_owner_document(scope, args.this());
let relevant_context = owner_document
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
.or_else(|| args.this().get_creation_context(scope))
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match create_document_value(
target_scope,
parsed.namespace_uri.as_deref(),
&qualified_name,
parsed.doctype,
) {
Some(value) => rv.set(value),
None => rv.set_undefined(),
}
@@ -12,7 +12,7 @@ struct DocumentCreateEventArgs {
interface: String,
}
#[derive(Debug, PartialEq, Eq, strum::EnumString)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
enum DocumentCreateEventKind {
BeforeUnloadEvent,
@@ -68,6 +68,31 @@ fn throw_not_supported_dom_exception(scope: &mut v8::PinScope<'_, '_>, message:
crate::context_bootstrap::throw_dom_exception_value(scope, message, "NotSupportedError");
}
fn new_uninitialized_document_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
kind: DocumentCreateEventKind,
) -> Option<v8::Local<'s, v8::Object>> {
if matches!(
kind,
DocumentCreateEventKind::BeforeUnloadEvent | DocumentCreateEventKind::TextEvent
) {
return match kind {
DocumentCreateEventKind::BeforeUnloadEvent => {
new_uninitialized_before_unload_event(scope)
}
DocumentCreateEventKind::TextEvent => new_uninitialized_text_event(scope),
_ => unreachable!(),
};
}
let global = scope.get_current_context().global(scope);
let constructor_value = global.get(scope, v8str(scope, kind.constructor_name()).into())?;
let constructor = v8::Local::<v8::Function>::try_from(constructor_value).ok()?;
let empty_type = v8str(scope, "");
let event = constructor.new_instance(scope, &[empty_type.into()])?;
set_event_initialized(scope, event, false);
Some(event)
}
pub(super) fn document_create_event_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -80,40 +105,11 @@ pub(super) fn document_create_event_callback<'s>(
throw_not_supported_dom_exception(scope, "The provided event type is not supported.");
return;
};
if matches!(
kind,
DocumentCreateEventKind::BeforeUnloadEvent | DocumentCreateEventKind::TextEvent
) {
let event = match kind {
DocumentCreateEventKind::BeforeUnloadEvent => {
new_uninitialized_before_unload_event(scope)
}
DocumentCreateEventKind::TextEvent => new_uninitialized_text_event(scope),
_ => unreachable!(),
};
match event {
Some(event) => rv.set(event.into()),
None => rv.set_undefined(),
}
return;
}
let ctor_name = kind.constructor_name();
let global = scope.get_current_context().global(scope);
let Some(constructor_value) = global.get(scope, v8str(scope, ctor_name).into()) else {
rv.set_undefined();
return;
};
let Ok(constructor) = v8::Local::<v8::Function>::try_from(constructor_value) else {
rv.set_undefined();
return;
};
let empty_type = v8str(scope, "");
match constructor.new_instance(scope, &[empty_type.into()]) {
Some(event) => {
set_event_initialized(scope, event, false);
rv.set(event.into());
}
let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this())
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match new_uninitialized_document_event(target_scope, kind) {
Some(event) => rv.set(event.into()),
None => rv.set_undefined(),
}
}
@@ -21,6 +21,24 @@ pub(in crate::context_bootstrap) fn current_document_object<'s>(
pub(in crate::context_bootstrap) fn new_range_for_document<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let relevant_context = crate::native_bridge::node_relevant_context(scope, document)?;
if relevant_context != scope.get_current_context() {
let document = v8::Global::new(scope, document);
let range = {
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let document = v8::Local::new(target_scope, &document);
let range = new_range_for_document_in_current_context(target_scope, document)?;
v8::Global::new(target_scope, range)
};
return Some(v8::Local::new(scope, &range));
}
new_range_for_document_in_current_context(scope, document)
}
fn new_range_for_document_in_current_context<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let global = scope.get_current_context().global(scope);
let ctor = global.get(scope, v8str(scope, "Range").into())?;
@@ -35,7 +35,20 @@ pub(in crate::native_bridge) fn node_create_attribute_callback<'s>(
} else {
parsed.name
};
match new_attr_object(scope, &name, "", None, Some(args.this()), None, None, &name) {
let document = args.this();
let relevant_context = crate::native_bridge::node_relevant_context(scope, document)
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match new_attr_object(
target_scope,
&name,
"",
None,
Some(document),
None,
None,
&name,
) {
Some(attr) => rv.set(attr.into()),
None => rv.set_null(),
}
@@ -70,12 +83,16 @@ pub(in crate::native_bridge) fn node_create_attribute_ns_callback<'s>(
return;
}
};
let document = args.this();
let relevant_context = crate::native_bridge::node_relevant_context(scope, document)
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match new_attr_object(
scope,
target_scope,
&parsed.qualified_name,
"",
None,
Some(args.this()),
Some(document),
namespace.as_deref(),
prefix.as_deref(),
&local_name,
@@ -1016,9 +1016,12 @@ pub(in crate::native_bridge::document) fn detached_document_implementation_gette
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let document = args.this();
let global = scope.get_current_context().global(scope);
let _ = crate::context_bootstrap::ensure_dom_implementation_singleton(scope, global);
match ensure_detached_document_implementation(scope, document) {
let relevant_context = crate::native_bridge::node_relevant_context(scope, document)
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let global = relevant_context.global(target_scope);
let _ = crate::context_bootstrap::ensure_dom_implementation_singleton(target_scope, global);
match ensure_detached_document_implementation(target_scope, document) {
Some(implementation) => rv.set(implementation.into()),
None => rv.set_null(),
}
@@ -53,9 +53,12 @@ pub(in crate::native_bridge) fn node_create_node_iterator_callback<'s>(
rv.set_null();
return;
};
let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this())
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let runtime = unsafe { &mut *runtime_ptr };
let iterator = traversal::build_node_iterator_wrapper(
scope,
target_scope,
runtime_ptr,
runtime.native_bridge_mut(),
root,
@@ -93,9 +96,12 @@ pub(in crate::native_bridge) fn node_create_tree_walker_callback<'s>(
rv.set_null();
return;
};
let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this())
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let runtime = unsafe { &mut *runtime_ptr };
let walker = traversal::build_tree_walker_wrapper(
scope,
target_scope,
runtime_ptr,
runtime.native_bridge_mut(),
root,
@@ -8,7 +8,9 @@ use crate::{
webidl,
};
use super::super::node::node_runtime_and_handle_from_object_or_detached;
use super::super::node::{
node_owner_document_relevant_context, node_runtime_and_handle_from_object_or_detached,
};
use super::{element_attribute, set_reflected_attribute};
const CANVAS_CONTEXT_KIND_SLOT: &str = "__moliCanvasContextKind";
@@ -31,6 +33,17 @@ fn canvas_context_slot(kind: CanvasContextKind) -> &'static str {
}
}
fn build_canvas_context_for_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
kind: CanvasContextKind,
) -> Option<v8::Local<'s, v8::Value>> {
match kind {
CanvasContextKind::TwoD => build_canvas_rendering_context_2d_object(scope).map(Into::into),
CanvasContextKind::WebGl => build_webgl_context_object(scope).map(Into::into),
CanvasContextKind::WebGl2 => build_webgl2_context_object(scope).map(Into::into),
}
}
pub(crate) fn html_canvas_width_getter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -179,28 +192,32 @@ pub(crate) fn canvas_get_context_callback<'s>(
}
}
let Some(value) = (match kind {
CanvasContextKind::TwoD => build_canvas_rendering_context_2d_object(scope).map(Into::into),
CanvasContextKind::WebGl => build_webgl_context_object(scope).map(Into::into),
CanvasContextKind::WebGl2 => build_webgl2_context_object(scope).map(Into::into),
}) else {
let relevant_context = node_runtime_and_handle_from_object_or_detached(scope, canvas)
.ok()
.and_then(|(runtime_ptr, handle)| {
node_owner_document_relevant_context(scope, runtime_ptr, handle)
})
.or_else(|| canvas.get_creation_context(scope))
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let Some(value) = build_canvas_context_for_kind(target_scope, kind) else {
rv.set_null();
return;
};
let Some(kind_value) = v8_string(scope, kind.label()) else {
let Some(kind_value) = v8_string(target_scope, kind.label()) else {
rv.set_null();
return;
};
let _ = canvas.define_own_property(
scope,
v8str(scope, CANVAS_CONTEXT_KIND_SLOT).into(),
target_scope,
v8str(target_scope, CANVAS_CONTEXT_KIND_SLOT).into(),
kind_value.into(),
v8::PropertyAttribute::DONT_ENUM,
);
let slot = canvas_context_slot(kind);
let _ = canvas.define_own_property(
scope,
v8str(scope, slot).into(),
target_scope,
v8str(target_scope, slot).into(),
value,
v8::PropertyAttribute::DONT_ENUM,
);
@@ -209,7 +226,7 @@ pub(crate) fn canvas_get_context_callback<'s>(
CanvasContextKind::TwoD | CanvasContextKind::WebGl | CanvasContextKind::WebGl2
) && let Ok(context) = v8::Local::<v8::Object>::try_from(value)
{
attach_canvas_like_context_object(scope, canvas, context);
attach_canvas_like_context_object(target_scope, canvas, context);
}
rv.set(value);
}
@@ -1,7 +1,7 @@
use super::super::{
CollectionKind, LiveCollectionQueryKind, collections, encode_tag_name_ns_query,
node::{
node_is_document, node_runtime_and_handle_from_args,
node_is_document, node_owner_document_relevant_context, node_runtime_and_handle_from_args,
node_runtime_and_handle_from_args_or_detached,
node_runtime_and_handle_from_object_or_detached, receiver_has_detached_state,
require_element_method_receiver, require_parent_node_receiver, set_wrapped_node_or_null,
@@ -170,7 +170,24 @@ pub(in crate::native_bridge) fn node_query_selector_all_callback<'s>(
};
match unsafe { &*runtime_ptr }.query_selector_all(Some(handle), &parsed.selectors) {
Ok(handles) => {
let list = collections::build_node_list_from_handles(scope, runtime_ptr, &handles);
// Default-world wrappers are shared across same-origin documents,
// so their creation context need not be their document's realm.
// Isolated-world wrappers instead retain their own world identity.
let receiver_context = args.this().get_creation_context(scope);
let isolated_context = receiver_context.filter(|context| {
let host = unsafe { &*runtime_ptr };
host.window_execution_context_identity_for_v8_context(scope, *context)
.is_some_and(|identity| {
!host.window_execution_context_identity_is_default_world(identity)
})
});
let relevant_context = isolated_context
.or_else(|| node_owner_document_relevant_context(scope, runtime_ptr, handle))
.or(receiver_context)
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
let list =
collections::build_node_list_from_handles(target_scope, runtime_ptr, &handles);
rv.set(list.into());
}
Err(error) => throw_native_selector_error_for_selector(scope, &parsed.selectors, &error),
@@ -51,6 +51,57 @@ fn child_document_create_text_and_comment_nodes_work() {
"function|3|#text|hello|true|function|8|#comment|note|2"
);
}
#[test]
fn child_document_factories_allocate_results_in_document_realm() {
let mut vm = new_storage_test_vm("https://child-document-factory-realm.test/");
let result = vm
.eval(
r#"
(() => {
const frame = document.createElement('iframe');
(document.body || document.documentElement || document).appendChild(frame);
const child = frame.contentWindow;
const doc = child.document;
const ChildObject = child.Object;
child.Object = Object;
const range = doc.createRange();
const canvas = doc.createElement('canvas');
const context = canvas.getContext('2d');
const createdImageData = context.createImageData(1, 1);
const readImageData = context.getImageData(0, 0, 1, 1);
const values = {
nodeList: doc.querySelectorAll('*'),
attribute: doc.createAttribute('data-value'),
namespacedAttribute: doc.createAttributeNS(null, 'data-value'),
event: doc.createEvent('Event'),
range,
clonedRange: range.cloneRange(),
nodeIterator: doc.createNodeIterator(doc),
treeWalker: doc.createTreeWalker(doc),
implementation: doc.implementation,
documentType: doc.implementation.createDocumentType('html', '', ''),
xmlDocument: doc.implementation.createDocument(null, 'root'),
htmlDocument: doc.implementation.createHTMLDocument('title'),
canvasContext: context,
createdImageData,
createdImageDataData: createdImageData.data,
readImageData,
readImageDataData: readImageData.data
};
return Object.entries(values)
.filter(([, value]) => !(value instanceof ChildObject) || value instanceof Object)
.map(([name]) => name)
.join(',');
})()
"#,
)
.expect("child document factory realm probe should evaluate");
assert_eq!(result, "");
}
#[test]
fn detached_character_data_accessors_parse_webidl_arguments() {
let mut vm = new_storage_test_vm("https://detached-character-data-webidl.test/");
@@ -5,6 +5,7 @@ mod cssom;
mod dom;
mod forms;
mod misc;
mod query_realms;
mod shadow_dom;
mod style_invalidation;
mod xhr;
@@ -0,0 +1,90 @@
use super::*;
#[test]
fn query_selector_all_preserves_isolated_world_node_identity() {
for universal_access in [false, true] {
let mut vm = new_parsed_test_vm(
"https://query-selector-realms.test/",
"<!doctype html><html><body></body></html>",
);
let context_id = vm
.create_isolated_world("query-selector-identity", universal_access)
.expect("isolated query realm should be created");
let result = vm
.eval_in_isolated_context(
context_id,
r#"
(() => {
const assert = (condition, message) => { if (!condition) throw new Error(message); };
const fragment = document.createDocumentFragment();
const detached = document.createElement('section');
const shadow = document.body.appendChild(document.createElement('div')).attachShadow({mode:'open'});
for (const root of [document.body, fragment, detached, shadow]) {
for (let index = 0; index < 1100; ++index) {
const node = document.createElement('span');
node.className = index === 0 ? 'hit first' : 'hit';
root.appendChild(node);
}
}
for (const root of [document, document.body, fragment, detached, shadow]) {
for (const selector of ['.first', '.hit']) {
const first = root.querySelector(selector);
const list = root.querySelectorAll(selector);
assert(list[0] === first, 'querySelector and querySelectorAll must return the same wrapper');
assert(Object.getPrototypeOf(list) === NodeList.prototype, 'NodeList belongs to the isolated receiver realm');
assert(list.item(0) === first && [...list][0] === first, 'item and iteration preserve node identity');
assert(list !== root.querySelectorAll(selector), 'each query creates a fresh static NodeList');
const length = list.length;
first.remove();
assert(list.length === length && list[0] === first, 'removed nodes remain in the static result');
root === document ? document.body.prepend(first) : root.prepend(first);
}
}
return 'ok';
})()
"#,
)
.expect("isolated queries must not fall back to the main world");
assert_eq!(result, "ok", "universal_access={universal_access}");
}
}
#[test]
fn query_selector_all_borrowed_methods_keep_the_receiver_realm() {
let mut vm = new_parsed_test_vm(
"https://query-selector-borrowed.test/",
"<!doctype html><html><body><span class=hit></span></body></html>",
);
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => { if (!condition) throw new Error(message); };
const frame = document.body.appendChild(document.createElement('iframe'));
const child = frame.contentWindow;
child.document.body.appendChild(child.document.createElement('span')).className = 'hit';
for (const [owner, caller] of [[window, child], [child, window]]) {
const doc = owner.document;
const fragment = doc.createDocumentFragment();
fragment.appendChild(doc.createElement('span')).className = 'hit';
const shadow = doc.body.appendChild(doc.createElement('div')).attachShadow({mode:'open'});
shadow.appendChild(doc.createElement('span')).className = 'hit';
for (const [root, method] of [
[doc, caller.Document.prototype.querySelectorAll],
[doc.body, caller.Element.prototype.querySelectorAll],
[fragment, caller.DocumentFragment.prototype.querySelectorAll],
[shadow, caller.DocumentFragment.prototype.querySelectorAll],
]) {
const list = method.call(root, '.hit');
assert(Object.getPrototypeOf(list) === owner.NodeList.prototype, 'result uses receiver realm, not borrowed method realm');
assert(list[0] === root.querySelector('.hit'), 'borrowed query preserves native node identity');
assert(list.item(0) === list[0] && [...list][0] === list[0], 'all collection access paths agree');
}
}
return 'ok';
})()
"#,
)
.expect("querySelectorAll must retain the receiver realm across borrowed methods");
assert_eq!(result, "ok");
}