fix(aria): resolve element reference attributes

This commit is contained in:
ldm0
2026-09-16 22:02:04 +08:00
parent a8399cae32
commit ca01d01d76
10 changed files with 575 additions and 42 deletions
@@ -2910,8 +2910,6 @@ html/browsers/windows/nested-browsing-contexts/window-top-null.html
html/browsers/windows/noreferrer-window-name.html
html/cross-origin-embedder-policy/coep-on-response-from-service-worker.https.html
html/cross-origin-embedder-policy/service-worker-cache-storage.https.html
html/dom/aria-element-reflection-disconnected.html
html/dom/aria-element-reflection.html
html/dom/documents/dom-tree-accessors/nameditem-01.html
html/dom/documents/dom-tree-accessors/nameditem-02.html
html/dom/documents/dom-tree-accessors/nameditem-04.html
@@ -5380,6 +5380,8 @@ html/cross-origin-embedder-policy/no-secure-context.html
html/cross-origin-opener-policy/no-https.html
html/dom/access-key-label.html
html/dom/aria-attribute-reflection.html
html/dom/aria-element-reflection-disconnected.html
html/dom/aria-element-reflection.html
html/dom/documents/dom-tree-accessors/Document.body.html
html/dom/documents/dom-tree-accessors/Document.getElementsByClassName-null-undef.html
html/dom/documents/dom-tree-accessors/Element.getElementsByClassName-null-undef.html
@@ -1,7 +1,9 @@
use super::Attribute;
use crate::forms::{InputValueSanitizationContext, sanitize_input_value_for_type_with_context};
use crate::native::NativeNodeId;
use indexmap::IndexSet;
use moli_html_input_type::InputType;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct SelectedFile {
@@ -123,6 +125,11 @@ impl ScriptElementState {
}
}
#[derive(Debug, Clone, Default)]
struct ExplicitAriaElementReferenceState {
references_by_attribute: HashMap<String, Vec<NativeNodeId>>,
}
#[derive(Debug, Clone, Default)]
pub struct ElementControlState {
input_value: Option<String>,
@@ -164,9 +171,39 @@ pub struct ElementControlState {
dialog_modal: bool,
dialog_return_value: String,
custom_states: IndexSet<String>,
explicit_aria_element_references: Option<Box<ExplicitAriaElementReferenceState>>,
}
impl ElementControlState {
pub fn explicit_aria_element_references(&self, attribute: &str) -> Option<&[NativeNodeId]> {
self.explicit_aria_element_references
.as_deref()?
.references_by_attribute
.get(attribute)
.map(Vec::as_slice)
}
pub fn set_explicit_aria_element_references(
&mut self,
attribute: &str,
references: Vec<NativeNodeId>,
) {
self.explicit_aria_element_references
.get_or_insert_with(Default::default)
.references_by_attribute
.insert(attribute.to_owned(), references);
}
pub fn clear_explicit_aria_element_references(&mut self, attribute: &str) {
let Some(references) = self.explicit_aria_element_references.as_mut() else {
return;
};
references.references_by_attribute.remove(attribute);
if references.references_by_attribute.is_empty() {
self.explicit_aria_element_references = None;
}
}
pub fn from_element_parts(
namespace: &str,
local_name: &str,
+40
View File
@@ -59,6 +59,20 @@ fn initial_custom_element_state_for_identity(
}
}
fn is_aria_element_reference_attribute(name: &str) -> bool {
matches!(
name,
"aria-activedescendant"
| "aria-controls"
| "aria-describedby"
| "aria-details"
| "aria-errormessage"
| "aria-flowto"
| "aria-labelledby"
| "aria-owns"
)
}
#[derive(Debug, Clone)]
pub struct Element {
local_name: LocalName,
@@ -167,6 +181,27 @@ impl Element {
self.prefix.as_ref().map(AsRef::as_ref)
}
pub fn explicit_aria_element_references(&self, attribute: &str) -> Option<&[NativeNodeId]> {
self.control_state()
.explicit_aria_element_references(attribute)
}
pub fn set_explicit_aria_element_references(
&mut self,
attribute: &str,
references: Vec<NativeNodeId>,
) {
self.control_state_mut()
.set_explicit_aria_element_references(attribute, references);
}
fn synchronize_aria_element_reference_attribute(&mut self, namespace: &str, local_name: &str) {
if namespace.is_empty() && is_aria_element_reference_attribute(local_name) {
self.rare_data
.clear_explicit_aria_element_references(local_name);
}
}
pub fn set_prefix(&mut self, prefix: Option<String>) -> bool {
if self.prefix.as_deref() == prefix.as_deref() {
return false;
@@ -1205,6 +1240,7 @@ impl Element {
prefix: Option<String>,
value: String,
) -> bool {
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
let next_value = value.clone();
if let Some(index) = self
.attributes
@@ -1254,6 +1290,7 @@ impl Element {
value: String,
units: Vec<u16>,
) -> bool {
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
let next_value = value.clone();
let value_utf16_units =
utf16_units_contain_unpaired_surrogate(&units).then(|| units.into_boxed_slice());
@@ -1305,6 +1342,7 @@ impl Element {
prefix: Option<String>,
value: String,
) -> bool {
self.synchronize_aria_element_reference_attribute(&namespace, &local_name);
let next_value = value.clone();
if let Some(index) = self.attributes.iter().position(|attribute| {
attribute.local_name() == local_name && attribute.namespace() == namespace
@@ -1337,6 +1375,7 @@ impl Element {
}
pub fn remove_attribute(&mut self, name: &str) -> bool {
self.synchronize_aria_element_reference_attribute("", name);
let Some(index) = self
.attributes
.iter()
@@ -1353,6 +1392,7 @@ impl Element {
}
pub fn remove_attribute_ns(&mut self, namespace: &str, local_name: &str) -> bool {
self.synchronize_aria_element_reference_attribute(namespace, local_name);
let Some(index) = self.attributes.iter().position(|attribute| {
attribute.namespace() == namespace && attribute.local_name() == local_name
}) else {
+10
View File
@@ -118,6 +118,16 @@ impl ElementRareData {
.get_or_insert_with(ElementControlState::default)
}
pub(super) fn clear_explicit_aria_element_references(&mut self, attribute: &str) {
if let Some(control_state) = self
.payload
.as_deref_mut()
.and_then(|payload| payload.control_state.as_mut())
{
control_state.clear_explicit_aria_element_references(attribute);
}
}
pub(super) fn custom_element_state(&self) -> CustomElementState {
self.payload
.as_deref()
+41
View File
@@ -154,6 +154,47 @@ fn template_contents_uses_and_releases_rare_data() {
assert!(!template.rare_data.is_materialized());
}
#[test]
fn aria_element_reference_state_is_owned_by_the_content_attribute() {
let mut element = Element::new_html("div");
assert!(element.set_attribute(
"aria-controls".to_owned(),
String::new(),
None,
String::new(),
));
element.set_explicit_aria_element_references("aria-controls", vec![NativeNodeId::new(7)]);
assert!(!element.set_attribute(
"aria-controls".to_owned(),
String::new(),
None,
String::new(),
));
assert_eq!(
element.explicit_aria_element_references("aria-controls"),
None
);
element.set_explicit_aria_element_references("aria-controls", vec![NativeNodeId::new(8)]);
assert!(element.set_attribute_ns(
"aria-controls".to_owned(),
"urn:example".to_owned(),
Some("x".to_owned()),
"foreign".to_owned(),
));
assert_eq!(
element.explicit_aria_element_references("aria-controls"),
Some([NativeNodeId::new(8)].as_slice())
);
assert!(element.remove_attribute("aria-controls"));
assert_eq!(
element.explicit_aria_element_references("aria-controls"),
None
);
}
#[test]
fn html_element_interface_name_covers_replay_tags() {
assert_eq!(html_element_interface_name("meta"), "HTMLMetaElement");
@@ -3,6 +3,33 @@ use std::sync::Arc;
use super::*;
impl DomHost {
pub fn explicit_aria_element_references(
&self,
handle: DomHandle,
attribute: &str,
) -> Option<Vec<DomHandle>> {
self.node(handle)?
.as_element()?
.explicit_aria_element_references(attribute)
.map(<[DomHandle]>::to_vec)
}
pub fn set_explicit_aria_element_references(
&mut self,
handle: DomHandle,
attribute: &str,
references: Vec<DomHandle>,
) -> bool {
let Some(element) = self
.node_mut(handle)
.and_then(|node| node.data_mut().as_element_mut())
else {
return false;
};
element.set_explicit_aria_element_references(attribute, references);
true
}
pub fn get_attribute(&self, handle: DomHandle, name: &str) -> Option<String> {
self.dom.get_attribute(handle, name)
}
+327 -23
View File
@@ -8,16 +8,16 @@ use super::super::{
v8_string, v8str,
},
};
use super::JsContextHost;
use super::node::{
element_name_for_owner_document, node_is_document, node_is_element,
node_runtime_and_handle_from_object, node_runtime_and_handle_from_object_or_detached,
node_text_content_getter_function, require_element_getter_receiver,
require_element_setter_receiver, set_text_content_in_reaction_scope,
throw_incompatible_getter_receiver, throw_incompatible_method_receiver,
throw_incompatible_setter_receiver,
current_or_live_delegate_node_arg_handle, element_name_for_owner_document, node_is_document,
node_is_element, node_runtime_and_handle_from_object,
node_runtime_and_handle_from_object_or_detached, node_text_content_getter_function,
require_element_getter_receiver, require_element_setter_receiver,
set_text_content_in_reaction_scope, throw_incompatible_getter_receiver,
throw_incompatible_method_receiver, throw_incompatible_setter_receiver,
};
use crate::{document_runtime::DomHandle, web_api_interfaces, webidl};
use super::{JsContextHost, wrapped_handle_value};
use crate::{custom_elements, document_runtime::DomHandle, web_api_interfaces, webidl};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiTemplateValue};
mod activation;
@@ -7527,14 +7527,217 @@ fn aria_string_attribute_setter_callback<'s>(
rv.set_undefined();
}
fn aria_element_reference_slot(attribute: &str) -> String {
format!("__moliAriaElementReference:{attribute}")
fn aria_element_reference_array_cache_slot(attribute: &str) -> String {
format!("__moliAriaElementReferenceArrayCache:{attribute}")
}
fn aria_element_reference_is_singular(attribute: &str) -> bool {
attribute == "aria-activedescendant"
}
struct AriaElementReferenceValue<'s> {
object: v8::Local<'s, v8::Object>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
}
impl<'s> webidl::WebIdlConverter<'s> for AriaElementReferenceValue<'s> {
type Options = ();
fn convert(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
_context: webidl::Context,
_options: &Self::Options,
) -> std::result::Result<Self, webidl::WebIdlError> {
let object = v8::Local::<v8::Object>::try_from(value).map_err(|_| {
webidl::WebIdlError::custom_message("ARIA element references must be Elements.")
})?;
let (runtime_ptr, handle) = node_runtime_and_handle_from_object_or_detached(scope, object)
.map_err(|_| {
webidl::WebIdlError::custom_message("ARIA element references must be Elements.")
})?;
if !node_is_element(unsafe { &*runtime_ptr }, handle) {
return Err(webidl::WebIdlError::custom_message(
"ARIA element references must be Elements.",
));
}
Ok(Self {
object,
runtime_ptr,
handle,
})
}
}
fn aria_element_reference_handle_for_owner(
scope: &mut v8::PinScope<'_, '_>,
owner_runtime_ptr: *mut JsContextHost,
reference: AriaElementReferenceValue<'_>,
) -> Option<DomHandle> {
current_or_live_delegate_node_arg_handle(scope, owner_runtime_ptr, reference.object.into())
.filter(|handle| node_is_element(unsafe { &*owner_runtime_ptr }, *handle))
.or_else(|| (reference.runtime_ptr == owner_runtime_ptr).then_some(reference.handle))
}
fn aria_element_reference_is_in_valid_scope(
runtime: &JsContextHost,
owner: DomHandle,
candidate: DomHandle,
) -> bool {
if !node_is_element(runtime, candidate) {
return false;
}
let Some(candidate_root) = runtime.dom_host().root_node_handle(candidate) else {
return false;
};
let Some(mut owner_root) = runtime.dom_host().root_node_handle(owner) else {
return false;
};
loop {
if candidate_root == owner_root {
return true;
}
if !runtime.dom_host().is_shadow_root(owner_root) {
return false;
}
let Some(host) = runtime.dom_host().shadow_root_host(owner_root) else {
return false;
};
let Some(next_root) = runtime.dom_host().root_node_handle(host) else {
return false;
};
owner_root = next_root;
}
}
fn aria_element_by_id_including_disconnected(
runtime: &JsContextHost,
owner: DomHandle,
id: &str,
) -> Option<DomHandle> {
if id.is_empty() {
return None;
}
let root = runtime.dom_host().root_node_handle(owner)?;
let mut stack = runtime
.dom_host()
.child_handles_reversed(root)
.collect::<Vec<_>>();
while let Some(candidate) = stack.pop() {
if node_is_element(runtime, candidate)
&& runtime.dom_host().get_attribute(candidate, "id").as_deref() == Some(id)
{
return Some(candidate);
}
stack.extend(runtime.dom_host().child_handles_reversed(candidate));
}
None
}
fn aria_element_reference_content_handles(
runtime: &JsContextHost,
owner: DomHandle,
attribute: &str,
) -> Option<Vec<DomHandle>> {
let value = runtime.dom_host().get_attribute(owner, attribute)?;
if aria_element_reference_is_singular(attribute) {
return Some(
aria_element_by_id_including_disconnected(runtime, owner, &value)
.into_iter()
.collect(),
);
}
Some(
value
.split([' ', '\t', '\n', '\r', '\u{000c}'])
.filter(|token| !token.is_empty())
.filter_map(|token| aria_element_by_id_including_disconnected(runtime, owner, token))
.collect(),
)
}
fn aria_element_reference_handles(
runtime: &JsContextHost,
owner: DomHandle,
attribute: &str,
) -> Option<Vec<DomHandle>> {
match runtime
.dom_host()
.explicit_aria_element_references(owner, attribute)
{
Some(references) => Some(
references
.into_iter()
.filter(|candidate| {
aria_element_reference_is_in_valid_scope(runtime, owner, *candidate)
})
.collect(),
),
None => aria_element_reference_content_handles(runtime, owner, attribute),
}
}
fn aria_element_reference_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
) -> Option<v8::Local<'s, v8::Value>> {
wrapped_handle_value(scope, runtime_ptr, handle)
}
fn aria_element_reference_array_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut JsContextHost,
handles: Vec<DomHandle>,
) -> Option<Vec<v8::Local<'s, v8::Value>>> {
handles
.into_iter()
.map(|handle| aria_element_reference_value(scope, runtime_ptr, handle))
.collect()
}
fn aria_cached_frozen_element_array<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
attribute: &str,
values: &[v8::Local<'s, v8::Value>],
) -> v8::Local<'s, v8::Array> {
let slot = aria_element_reference_array_cache_slot(attribute);
if let Some(cached) = get_private_value(scope, receiver, &slot)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
&& cached.length() as usize == values.len()
{
let mut equal = true;
for (index, expected) in values.iter().copied().enumerate() {
if !cached
.get_index(scope, index as u32)
.is_some_and(|actual| actual.strict_equals(expected))
{
equal = false;
break;
}
}
if equal {
return cached;
}
}
let array = v8::Array::new_with_elements(scope, values);
let _ = array.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
set_private_value(scope, receiver, &slot, array.into());
array
}
fn clear_aria_element_reference_array_cache(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
attribute: &str,
) {
let slot = aria_element_reference_array_cache_slot(attribute);
let undefined = v8::undefined(scope);
set_private_value(scope, receiver, &slot, undefined.into());
}
fn aria_element_reference_attribute_getter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -7544,15 +7747,85 @@ fn aria_element_reference_attribute_getter_callback<'s>(
rv.set_null();
return;
};
let slot = aria_element_reference_slot(&attribute);
if let Some(value) = get_private_value(scope, args.this(), &slot) {
rv.set(value);
let Some((runtime_ptr, owner)) = element_getter_receiver(scope, args.this(), &attribute) else {
return;
};
let Some(handles) = aria_element_reference_handles(unsafe { &*runtime_ptr }, owner, &attribute)
else {
clear_aria_element_reference_array_cache(scope, args.this(), &attribute);
rv.set_null();
return;
};
if aria_element_reference_is_singular(&attribute) {
match handles
.into_iter()
.next()
.and_then(|handle| aria_element_reference_value(scope, runtime_ptr, handle))
{
Some(value) => rv.set(value),
None => rv.set_null(),
}
return;
}
if aria_element_reference_is_singular(&attribute) {
let Some(values) = aria_element_reference_array_values(scope, runtime_ptr, handles) else {
rv.set_null();
} else {
rv.set(v8::Array::new(scope, 0).into());
return;
};
let array = aria_cached_frozen_element_array(scope, args.this(), &attribute, &values);
rv.set(array.into());
}
fn set_explicit_aria_element_references(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
owner: DomHandle,
attribute: &str,
handles: Vec<DomHandle>,
) {
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
let _ = unsafe { &mut *runtime_ptr }.set_attribute_appending_to_current_reaction_queue(
scope,
runtime_ptr,
owner,
attribute,
"",
);
let _ = unsafe { &mut *runtime_ptr }
.dom_host_mut()
.set_explicit_aria_element_references(owner, attribute, handles);
});
}
fn clear_explicit_aria_element_references(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
owner: DomHandle,
attribute: &str,
) {
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
let _ = unsafe { &mut *runtime_ptr }.remove_attribute_appending_to_current_reaction_queue(
scope,
runtime_ptr,
owner,
attribute,
);
});
}
fn converted_aria_element_reference<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> Option<AriaElementReferenceValue<'s>> {
match webidl::convert::<AriaElementReferenceValue<'s>>(
scope,
value,
webidl::Context::argument("Element ARIA element reflection", 1),
) {
Ok(reference) => Some(reference),
Err(error) => {
webidl::throw_error(scope, &error);
None
}
}
}
@@ -7565,14 +7838,45 @@ fn aria_element_reference_attribute_setter_callback<'s>(
rv.set_undefined();
return;
};
let slot = aria_element_reference_slot(&attribute);
set_private_value(scope, args.this(), &slot, args.get(0));
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
rv.set_undefined();
let Some((runtime_ptr, owner)) = element_setter_receiver(scope, args.this(), &attribute) else {
return;
};
let _ = unsafe { &mut *runtime_ptr }.set_attribute(scope, runtime_ptr, handle, &attribute, "");
let value = args.get(0);
if value.is_null_or_undefined() {
clear_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute);
rv.set_undefined();
return;
}
if aria_element_reference_is_singular(&attribute) {
let Some(reference) = converted_aria_element_reference(scope, value) else {
return;
};
let handles = aria_element_reference_handle_for_owner(scope, runtime_ptr, reference)
.into_iter()
.collect();
set_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute, handles);
rv.set_undefined();
return;
}
let sequence = match webidl::convert::<webidl::Sequence<AriaElementReferenceValue<'s>>>(
scope,
value,
webidl::Context::argument("Element ARIA element reflection", 1),
) {
Ok(sequence) => sequence,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let mut handles = Vec::with_capacity(sequence.0.len());
for reference in sequence.0 {
if let Some(handle) = aria_element_reference_handle_for_owner(scope, runtime_ptr, reference)
&& !handles.contains(&handle)
{
handles.push(handle);
}
}
set_explicit_aria_element_references(scope, runtime_ptr, owner, &attribute, handles);
rv.set_undefined();
}
@@ -6063,32 +6063,102 @@ fn custom_element_popover_reflection_is_visible_on_html_element_prototype() {
}
#[test]
fn element_aria_element_reference_reflection_preserves_assigned_value() {
fn element_aria_element_reference_reflection_tracks_dom_owned_associations() {
let mut vm = new_storage_test_vm("https://example.com/");
let result = vm
.eval(
r#"
r##"
(() => {
const target = document.createElement("div");
const element = document.createElement("div");
const defaultReferences = element.ariaDescribedByElements;
element.ariaControlsElements = [target];
element.ariaActiveDescendantElement = target;
const probe = callback => {
try {
callback();
return "ok";
} catch (error) {
return error && error.name;
}
};
const html = document.documentElement ||
document.appendChild(document.createElement("html"));
const body = document.body ||
html.appendChild(document.createElement("body"));
const container = document.createElement("div");
container.innerHTML = `
<input aria-activedescendant="first" aria-describedby="first second">
<p id="first"></p><p id="second"></p>`;
body.appendChild(container);
const element = container.querySelector("input");
const first = container.querySelector("#first");
const second = container.querySelector("#second");
const initialDescriptions = element.ariaDescribedByElements;
const initialActive = element.ariaActiveDescendantElement;
container.remove();
const disconnectedDescriptions = element.ariaDescribedByElements;
const disconnectedActive = element.ariaActiveDescendantElement;
body.appendChild(container);
const assigned = [first, second];
element.ariaControlsElements = assigned;
assigned.pop();
const explicitControlsAttributeBlank =
element.getAttribute("aria-controls") === "";
const controls = element.ariaControlsElements;
const cachedControls = element.ariaControlsElements;
container.remove();
const disconnectedControls = element.ariaControlsElements;
body.appendChild(container);
element.setAttribute("aria-controls", "second");
const contentAttributeControls = element.ariaControlsElements;
element.removeAttribute("aria-controls");
element.ariaActiveDescendantElement = first;
const explicitActive = element.ariaActiveDescendantElement;
element.ariaActiveDescendantElement = null;
const missing = document.createElement("div");
const reactions = [];
class AriaReferenceElement extends HTMLElement {
static observedAttributes = ["aria-controls"];
attributeChangedCallback() {
reactions.push(this.ariaControlsElements?.[0] === first);
}
}
customElements.define("aria-reference-element", AriaReferenceElement);
const custom = document.createElement("aria-reference-element");
body.appendChild(custom);
custom.ariaControlsElements = [first];
return [
Array.isArray(defaultReferences),
defaultReferences.length,
element.getAttribute("aria-controls") === "",
element.ariaControlsElements[0] === target,
element.getAttribute("aria-activedescendant") === "",
element.ariaActiveDescendantElement === target
missing.ariaDescribedByElements === null,
initialDescriptions.length === 2 && initialDescriptions[0] === first && initialDescriptions[1] === second,
initialActive === first,
disconnectedDescriptions.length === 2 && disconnectedDescriptions[0] === first && disconnectedDescriptions[1] === second,
disconnectedActive === first,
explicitControlsAttributeBlank,
controls.length === 2 && controls[0] === first && controls[1] === second,
Object.isFrozen(controls),
controls === cachedControls,
disconnectedControls === controls,
contentAttributeControls.length === 1 && contentAttributeControls[0] === second,
element.ariaControlsElements === null,
explicitActive === first,
element.ariaActiveDescendantElement === null,
!element.hasAttribute("aria-activedescendant"),
probe(() => { element.ariaActiveDescendantElement = "not an element"; }),
probe(() => { element.ariaControlsElements = [first, 1]; }),
probe(() => { element.ariaControlsElements = first; }),
reactions.length === 1 && reactions[0]
].join("|");
})()
"#,
"##,
)
.expect("ARIA element reference reflection probe should evaluate");
assert_eq!(result, "true|0|true|true|true|true");
assert_eq!(
result,
"true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|TypeError|TypeError|TypeError|true"
);
}
#[test]
@@ -2677,7 +2677,11 @@ fn detached_reflected_element_attributes_use_owner_prototype_accessors() {
assert(section.getAttribute("aria-label") === "Proxy label", "proxy aria-label attribute");
const controls = [div];
section.ariaControlsElements = controls;
assert(section.ariaControlsElements === controls, "proxy ariaControlsElements");
const reflectedControls = section.ariaControlsElements;
assert(reflectedControls !== controls, "proxy ariaControlsElements snapshots input");
assert(reflectedControls.length === 1 && reflectedControls[0] === div, "proxy ariaControlsElements");
assert(Object.isFrozen(reflectedControls), "proxy ariaControlsElements frozen");
assert(section.ariaControlsElements === reflectedControls, "proxy ariaControlsElements cached");
assert(section.getAttribute("aria-controls") === "", "proxy aria-controls attribute");
assert(!own(section, "ariaControlsElements"), "ariaControlsElements should stay inherited after set");
assert(section.contentEditable === "plaintext-only", "proxy contentEditable");
@@ -2709,7 +2713,7 @@ fn detached_reflected_element_attributes_use_owner_prototype_accessors() {
assert(section.attributes.getNamedItem("data-x").value === "1", "proxy attributes after delete");
assert(section.shadowRoot === root, "proxy shadowRoot after delete");
assert(section.ariaLabel === "Proxy label", "proxy ariaLabel after delete");
assert(section.ariaControlsElements === controls, "proxy ariaControlsElements after delete");
assert(section.ariaControlsElements === reflectedControls, "proxy ariaControlsElements after delete");
assert(section.contentEditable === "plaintext-only", "proxy contentEditable after delete");
assert(section.isContentEditable === true, "proxy isContentEditable after delete");
assert(section.getHTML() === "<b>x</b>", "proxy getHTML after delete");