Compare commits

...

2 Commits

Author SHA1 Message Date
Alice
8ced3d1b8e layout: Expose some non-interactive accessibility roles based on DOM node type. (#44255)
Sets the accessibility role of nodes based on their DOM node type
(ignoring ARIA roles and computed styles for now), for a small subset of
roles.

This change also re-works some details of accessibility tree building:
- Set the `root` property for the accesskit tree to the node ID for the
root DOM node, rather than creating a placeholder root node which
contains the root DOM node.
- Since we now map the `<body>` node to the `RootWebArea` role, this
required a small change to the accessibility test logic to do a tree
walk to find the `RootWebArea` node, which used to be the first child
since we artificially set the placeholder root node to be the
`RootWebArea` node.
- Have `update_node_and_children()` return a bool indicating whether any
node was updated.
- Split the code updating the node itself into its own function,
`update_node()`, which takes a DOM node and returns a tuple containing
the accessibility node ID and a bool indicating whether the node
required updating
- Add an `assert_node_by_id()` method to retrieve an accessibility node
based on its ID.

Testing: See new tests added in this PR.

---------

Signed-off-by: Alice Boxhall <alice@igalia.com>
Co-authored-by: delan azabani <dazabani@igalia.com>
2026-04-25 06:25:20 +00:00
Gae24
1464ffd68a script: pass &mut JSContext to WritableStreamDefaultController::setup (#44490)
Also port `TextDecoderStream`, `TextEncoderStream`, `CompressionStream`
and `DecompressionStream` to `reflect_dom_object_with_proto_and_cx`.

Testing: It compiles
Part of #40600

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
2026-04-25 03:52:36 +00:00
10 changed files with 254 additions and 153 deletions

View File

@@ -1,14 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::sync::LazyLock;
use accesskit::Role;
use layout_api::LayoutNode;
use layout_api::{LayoutElement, LayoutNode, LayoutNodeType};
use log::trace;
use rustc_hash::FxHashMap;
use script::layout_dom::ServoLayoutNode;
use servo_base::Epoch;
use style::dom::{NodeInfo, OpaqueNode};
use style::dom::OpaqueNode;
use web_atoms::{LocalName, local_name};
use crate::ArcRefCell;
@@ -35,47 +37,28 @@ impl AccessibilityTree {
const ROOT_NODE_ID: accesskit::NodeId = accesskit::NodeId(0);
pub(super) fn new(tree_id: accesskit::TreeId, epoch: Epoch) -> Self {
// The root node doesn't correspond to a DOM node, but contains the root DOM node.
let mut root_node = AccessibilityNode::new(AccessibilityTree::ROOT_NODE_ID);
root_node
.accesskit_node
.set_role(accesskit::Role::RootWebArea);
root_node
.accesskit_node
.add_action(accesskit::Action::Focus);
let mut tree = Self {
nodes: Default::default(),
accesskit_tree: accesskit::Tree::new(root_node.id),
Self {
nodes: FxHashMap::default(),
accesskit_tree: accesskit::Tree::new(AccessibilityTree::ROOT_NODE_ID),
tree_id,
epoch,
};
tree.nodes.insert(root_node.id, ArcRefCell::new(root_node));
tree
}
}
pub(super) fn update_tree(
&mut self,
root_dom_node: &ServoLayoutNode<'_>,
) -> Option<accesskit::TreeUpdate> {
let root_node = self.get_or_create_node(root_dom_node);
self.accesskit_tree.root = root_node.borrow().id;
let mut tree_update = AccessibilityUpdate::new(self.accesskit_tree.clone(), self.tree_id);
let any_node_updated = self.update_node_and_children(root_dom_node, &mut tree_update);
{
let root_dom_node_id = Self::to_accesskit_id(&root_dom_node.opaque());
let root_node = self
.nodes
.get_mut(&AccessibilityTree::ROOT_NODE_ID)
.expect("Guaranteed by Self::new");
let mut root_node = root_node.borrow_mut();
root_node
.accesskit_node
.set_children(vec![root_dom_node_id]);
tree_update.add(&root_node);
if !any_node_updated {
return None;
}
self.update_node_and_children(root_dom_node, &mut tree_update);
Some(tree_update.finalize())
}
@@ -83,9 +66,26 @@ impl AccessibilityTree {
&mut self,
dom_node: &ServoLayoutNode<'_>,
tree_update: &mut AccessibilityUpdate,
) {
// TODO: read accessibility damage from dom_node (right now, assume damage is complete)
) -> bool {
// TODO: read accessibility damage (right now, assume damage is complete)
let (node_id, updated) = self.update_node(dom_node);
let mut any_descendant_updated = false;
for dom_child in dom_node.flat_tree_children() {
// TODO: We actually need to propagate damage within the accessibility tree, rather than
// assuming it matches the DOM tree, but this will do for now.
any_descendant_updated |= self.update_node_and_children(&dom_child, tree_update);
}
if updated {
let node = self.assert_node_by_id(node_id);
tree_update.add(&node.borrow());
}
updated || any_descendant_updated
}
fn update_node(&mut self, dom_node: &ServoLayoutNode<'_>) -> (accesskit::NodeId, bool) {
let node = self.get_or_create_node(dom_node);
let mut node = node.borrow_mut();
let accesskit_node = &mut node.accesskit_node;
@@ -99,21 +99,26 @@ impl AccessibilityTree {
accesskit_node.set_children(new_children);
}
if dom_node.is_text_node() {
if let Some(dom_element) = dom_node.as_element() {
accesskit_node.set_role(Role::GenericContainer);
let local_name = dom_element.local_name().to_ascii_lowercase();
accesskit_node.set_html_tag(&*local_name);
let role = HTML_ELEMENT_ROLE_MAPPINGS
.get(&local_name)
.unwrap_or(&Role::GenericContainer);
accesskit_node.set_role(*role);
} else if dom_node.type_id() == Some(LayoutNodeType::Text) {
accesskit_node.set_role(Role::TextRun);
let text_content = dom_node.text_content();
trace!("node text content = {text_content:?}");
// FIXME: this should take into account editing selection units (grapheme clusters?)
accesskit_node.set_value(&*text_content);
} else if dom_node.as_element().is_some() {
} else {
accesskit_node.set_role(Role::GenericContainer);
}
tree_update.add(&node);
for dom_child in dom_node.flat_tree_children() {
self.update_node_and_children(&dom_child, tree_update);
}
// TODO: only return true if any update actually happened
(node.id, true)
}
fn get_or_create_node(
@@ -129,6 +134,16 @@ impl AccessibilityTree {
node.clone()
}
fn assert_node_by_id(&self, id: accesskit::NodeId) -> ArcRefCell<AccessibilityNode> {
let Some(node) = self.nodes.get(&id) else {
panic!("Stale node ID found: {id:?}");
};
node.clone()
}
// TODO: Using the OpaqueNode as the identifier for the accessibility node will inevitably
// create issues as OpaqueNodes will be reused when DOM nodes are destroyed. Instead, we should
// make a monotonically increasing ID, and have some other way to retrieve it based on DOM node.
fn to_accesskit_id(opaque: &OpaqueNode) -> accesskit::NodeId {
accesskit::NodeId(opaque.0 as u64)
}
@@ -229,3 +244,25 @@ fn test_accessibility_update_add_some_nodes_twice() {
}
);
}
static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
[
(local_name!("article"), Role::Article),
(local_name!("aside"), Role::Complementary),
(local_name!("body"), Role::RootWebArea),
(local_name!("footer"), Role::ContentInfo),
(local_name!("h1"), Role::Heading),
(local_name!("h2"), Role::Heading),
(local_name!("h3"), Role::Heading),
(local_name!("h4"), Role::Heading),
(local_name!("h5"), Role::Heading),
(local_name!("h6"), Role::Heading),
(local_name!("header"), Role::Banner),
(local_name!("hr"), Role::Splitter),
(local_name!("main"), Role::Main),
(local_name!("nav"), Role::Navigation),
(local_name!("p"), Role::Paragraph),
]
.into_iter()
.collect()
});

View File

@@ -15,14 +15,14 @@ use crate::dom::bindings::codegen::Bindings::TextDecoderBinding;
use crate::dom::bindings::codegen::Bindings::TextDecoderStreamBinding::TextDecoderStreamMethods;
use crate::dom::bindings::codegen::UnionTypes::ArrayBufferViewOrArrayBuffer;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::encoding::textdecodercommon::TextDecoderCommon;
use crate::dom::globalscope::GlobalScope;
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
use crate::dom::types::{TransformStream, TransformStreamDefaultController};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
use crate::script_runtime::CanGc;
/// <https://encoding.spec.whatwg.org/#decode-and-enqueue-a-chunk>
#[expect(unsafe_code)]
@@ -122,25 +122,24 @@ impl TextDecoderStream {
}
fn new_with_proto(
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
encoding: &'static Encoding,
fatal: bool,
ignoreBOM: bool,
can_gc: CanGc,
) -> Fallible<DomRoot<Self>> {
let decoder = Rc::new(TextDecoderCommon::new_inherited(encoding, fatal, ignoreBOM));
let transformer_type = TransformerType::Decoder(decoder.clone());
let transform_stream = TransformStream::new_with_proto(global, None, can_gc);
transform_stream.set_up(cx, global, transformer_type, can_gc)?;
let transform_stream = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
transform_stream.set_up(cx, global, transformer_type)?;
Ok(reflect_dom_object_with_proto(
Ok(reflect_dom_object_with_proto_and_cx(
Box::new(TextDecoderStream::new_inherited(decoder, &transform_stream)),
global,
proto,
can_gc,
cx,
))
}
}
@@ -148,9 +147,9 @@ impl TextDecoderStream {
impl TextDecoderStreamMethods<crate::DomTypeHolder> for TextDecoderStream {
/// <https://encoding.spec.whatwg.org/#dom-textdecoderstream>
fn Constructor(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
label: DOMString,
options: &TextDecoderBinding::TextDecoderOptions,
) -> Fallible<DomRoot<TextDecoderStream>> {
@@ -164,13 +163,12 @@ impl TextDecoderStreamMethods<crate::DomTypeHolder> for TextDecoderStream {
};
Self::new_with_proto(
GlobalScope::get_cx(),
cx,
global,
proto,
encoding,
options.fatal,
options.ignoreBOM,
can_gc,
)
}

View File

@@ -23,13 +23,14 @@ use script_bindings::conversions::SafeToJSValConvertible;
use crate::dom::bindings::buffer_source::create_buffer_source;
use crate::dom::bindings::codegen::Bindings::TextEncoderStreamBinding::TextEncoderStreamMethods;
use crate::dom::bindings::error::{Error, Fallible, throw_dom_exception};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::stream::readablestream::ReadableStream;
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
use crate::dom::stream::writablestream::WritableStream;
use crate::dom::types::{GlobalScope, TransformStream, TransformStreamDefaultController};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
use crate::{DomTypeHolder, DomTypes};
/// String converted from an input JS Value
enum ConvertedInput<'a> {
@@ -341,10 +342,9 @@ impl TextEncoderStream {
/// <https://encoding.spec.whatwg.org/#dom-textencoderstream>
fn new_with_proto(
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
) -> Fallible<DomRoot<TextEncoderStream>> {
// Step 1. Set thiss encoder to an instance of the UTF-8 encoder.
let encoder = Encoder::default();
@@ -356,29 +356,29 @@ impl TextEncoderStream {
let transformer_type = TransformerType::Encoder(encoder);
// Step 4. Let transformStream be a new TransformStream.
let transform = TransformStream::new_with_proto(global, None, can_gc);
let transform = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
// Step 5. Set up transformStream with transformAlgorithm set to transformAlgorithm
// and flushAlgorithm set to flushAlgorithm.
transform.set_up(cx, global, transformer_type, can_gc)?;
transform.set_up(cx, global, transformer_type)?;
// Step 6. Set thiss transform to transformStream.
Ok(reflect_dom_object_with_proto(
Ok(reflect_dom_object_with_proto_and_cx(
Box::new(TextEncoderStream::new_inherited(&transform)),
global,
proto,
can_gc,
cx,
))
}
}
impl TextEncoderStreamMethods<DomTypeHolder> for TextEncoderStream {
impl TextEncoderStreamMethods<crate::DomTypeHolder> for TextEncoderStream {
/// <https://encoding.spec.whatwg.org/#dom-textencoderstream>
fn Constructor(
global: &<DomTypeHolder as DomTypes>::GlobalScope,
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
) -> Fallible<DomRoot<<DomTypeHolder as DomTypes>::TextEncoderStream>> {
TextEncoderStream::new_with_proto(GlobalScope::get_cx(), global, proto, can_gc)
) -> Fallible<DomRoot<TextEncoderStream>> {
TextEncoderStream::new_with_proto(cx, global, proto)
}
/// <https://encoding.spec.whatwg.org/#dom-textencoder-encoding>
@@ -388,12 +388,12 @@ impl TextEncoderStreamMethods<DomTypeHolder> for TextEncoderStream {
}
/// <https://streams.spec.whatwg.org/#dom-generictransformstream-readable>
fn Readable(&self) -> DomRoot<<DomTypeHolder as DomTypes>::ReadableStream> {
fn Readable(&self) -> DomRoot<ReadableStream> {
self.transform.get_readable()
}
/// <https://streams.spec.whatwg.org/#dom-generictransformstream-writable>
fn Writable(&self) -> DomRoot<<DomTypeHolder as DomTypes>::WritableStream> {
fn Writable(&self) -> DomRoot<WritableStream> {
self.transform.get_writable()
}
}

View File

@@ -24,7 +24,7 @@ use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::{
use crate::dom::bindings::codegen::UnionTypes::ArrayBufferViewOrArrayBuffer;
use crate::dom::bindings::conversions::{SafeFromJSValConvertible, SafeToJSValConvertible};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
use crate::dom::types::{
@@ -63,17 +63,17 @@ impl CompressionStream {
}
fn new_with_proto(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
transform: &TransformStream,
format: CompressionFormat,
can_gc: CanGc,
) -> DomRoot<CompressionStream> {
reflect_dom_object_with_proto(
reflect_dom_object_with_proto_and_cx(
Box::new(CompressionStream::new_inherited(transform, format)),
global,
proto,
can_gc,
cx,
)
}
}
@@ -81,9 +81,9 @@ impl CompressionStream {
impl CompressionStreamMethods<crate::DomTypeHolder> for CompressionStream {
/// <https://compression.spec.whatwg.org/#dom-compressionstream-compressionstream>
fn Constructor(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
format: CompressionFormat,
) -> Fallible<DomRoot<CompressionStream>> {
// Step 1. If format is unsupported in CompressionStream, then throw a TypeError.
@@ -91,9 +91,9 @@ impl CompressionStreamMethods<crate::DomTypeHolder> for CompressionStream {
// Step 2. Set thiss format to format.
// Step 5. Set thiss transform to a new TransformStream.
let transform = TransformStream::new_with_proto(global, None, can_gc);
let transform = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
let compression_stream =
CompressionStream::new_with_proto(global, proto, &transform, format, can_gc);
CompressionStream::new_with_proto(cx, global, proto, &transform, format);
// Step 3. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the
// compress and enqueue a chunk algorithm with this and chunk.
@@ -103,8 +103,7 @@ impl CompressionStreamMethods<crate::DomTypeHolder> for CompressionStream {
// Step 6. Set up thiss transform with transformAlgorithm set to transformAlgorithm and
// flushAlgorithm set to flushAlgorithm.
let cx = GlobalScope::get_cx();
transform.set_up(cx, global, transformer_type, can_gc)?;
transform.set_up(cx, global, transformer_type)?;
Ok(compression_stream)
}

View File

@@ -20,7 +20,7 @@ use crate::dom::bindings::codegen::Bindings::CompressionStreamBinding::Compressi
use crate::dom::bindings::codegen::Bindings::DecompressionStreamBinding::DecompressionStreamMethods;
use crate::dom::bindings::conversions::SafeToJSValConvertible;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::stream::compressionstream::{BROTLI_BUFFER_SIZE, convert_chunk_to_vec};
use crate::dom::stream::transformstreamdefaultcontroller::TransformerType;
@@ -59,17 +59,17 @@ impl DecompressionStream {
}
fn new_with_proto(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
transform: &TransformStream,
format: CompressionFormat,
can_gc: CanGc,
) -> DomRoot<DecompressionStream> {
reflect_dom_object_with_proto(
reflect_dom_object_with_proto_and_cx(
Box::new(DecompressionStream::new_inherited(transform, format)),
global,
proto,
can_gc,
cx,
)
}
}
@@ -77,9 +77,9 @@ impl DecompressionStream {
impl DecompressionStreamMethods<crate::DomTypeHolder> for DecompressionStream {
/// <https://compression.spec.whatwg.org/#dom-decompressionstream-decompressionstream>
fn Constructor(
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
format: CompressionFormat,
) -> Fallible<DomRoot<DecompressionStream>> {
// Step 1. If format is unsupported in DecompressionStream, then throw a TypeError.
@@ -87,9 +87,9 @@ impl DecompressionStreamMethods<crate::DomTypeHolder> for DecompressionStream {
// Step 2. Set thiss format to format.
// Step 5. Set thiss transform to a new TransformStream.
let transform = TransformStream::new_with_proto(global, None, can_gc);
let transform = TransformStream::new_with_proto(global, None, CanGc::from_cx(cx));
let decompression_stream =
DecompressionStream::new_with_proto(global, proto, &transform, format, can_gc);
DecompressionStream::new_with_proto(cx, global, proto, &transform, format);
// Step 3. Let transformAlgorithm be an algorithm which takes a chunk argument and runs the
// decompress and enqueue a chunk algorithm with this and chunk.
@@ -99,8 +99,7 @@ impl DecompressionStreamMethods<crate::DomTypeHolder> for DecompressionStream {
// Step 6. Set up thiss transform with transformAlgorithm set to transformAlgorithm and
// flushAlgorithm set to flushAlgorithm.
let cx = GlobalScope::get_cx();
transform.set_up(cx, global, transformer_type, can_gc)?;
transform.set_up(cx, global, transformer_type)?;
Ok(decompression_stream)
}

View File

@@ -451,22 +451,23 @@ impl TransformStream {
/// <https://streams.spec.whatwg.org/#transformstream-set-up>
pub(crate) fn set_up(
&self,
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
transformer_type: TransformerType,
can_gc: CanGc,
) -> Fallible<()> {
// Step1. Let writableHighWaterMark be 1.
let writable_high_water_mark = 1.0;
// Step 2. Let writableSizeAlgorithm be an algorithm that returns 1.
let writable_size_algorithm = extract_size_algorithm(&Default::default(), can_gc);
let writable_size_algorithm =
extract_size_algorithm(&Default::default(), CanGc::from_cx(cx));
// Step 3. Let readableHighWaterMark be 0.
let readable_high_water_mark = 0.0;
// Step 4. Let readableSizeAlgorithm be an algorithm that returns 1.
let readable_size_algorithm = extract_size_algorithm(&Default::default(), can_gc);
let readable_size_algorithm =
extract_size_algorithm(&Default::default(), CanGc::from_cx(cx));
// Step 5. Let transformAlgorithmWrapper be an algorithm that runs these steps given a value chunk:
// Step 6. Let flushAlgorithmWrapper be an algorithm that runs these steps:
@@ -474,7 +475,7 @@ impl TransformStream {
// NOTE: These steps are implemented in `TransformStreamDefaultController::new`
// Step 8. Let startPromise be a promise resolved with undefined.
let start_promise = Promise::new_resolved(global, cx, (), can_gc);
let start_promise = Promise::new_resolved(global, cx.into(), (), CanGc::from_cx(cx));
// Step 9. Perform ! InitializeTransformStream(stream, startPromise,
// writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark,
@@ -487,11 +488,11 @@ impl TransformStream {
writable_size_algorithm,
readable_high_water_mark,
readable_size_algorithm,
can_gc,
)?;
// Step 10. Let controller be a new TransformStreamDefaultController.
let controller = TransformStreamDefaultController::new(global, transformer_type, can_gc);
let controller =
TransformStreamDefaultController::new(global, transformer_type, CanGc::from_cx(cx));
// Step 11. Perform ! SetUpTransformStreamDefaultController(stream,
// controller, transformAlgorithmWrapper, flushAlgorithmWrapper,
@@ -518,17 +519,16 @@ impl TransformStream {
}
/// <https://streams.spec.whatwg.org/#initialize-transform-stream>
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn initialize(
&self,
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
start_promise: Rc<Promise>,
writable_high_water_mark: f64,
writable_size_algorithm: Rc<QueuingStrategySize>,
readable_high_water_mark: f64,
readable_size_algorithm: Rc<QueuingStrategySize>,
can_gc: CanGc,
) -> Fallible<()> {
// Let startAlgorithm be an algorithm that returns startPromise.
// Let writeAlgorithm be the following steps, taking a chunk argument:
@@ -547,7 +547,6 @@ impl TransformStream {
writable_high_water_mark,
writable_size_algorithm,
UnderlyingSinkType::Transform(Dom::from_ref(self), start_promise.clone()),
can_gc,
)?;
self.writable.set(Some(&writable));
@@ -567,7 +566,7 @@ impl TransformStream {
UnderlyingSourceType::Transform(Dom::from_ref(self), start_promise),
Some(readable_size_algorithm),
Some(readable_high_water_mark),
can_gc,
CanGc::from_cx(cx),
);
self.readable.set(Some(&readable));
@@ -575,7 +574,7 @@ impl TransformStream {
// Note: This is done in the constructor.
// Perform ! TransformStreamSetBackpressure(stream, true).
self.set_backpressure(global, true, can_gc);
self.set_backpressure(global, true, CanGc::from_cx(cx));
// Set stream.[[controller]] to undefined.
self.controller.set(None);
@@ -962,22 +961,21 @@ impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
/// <https://streams.spec.whatwg.org/#ts-constructor>
#[expect(unsafe_code)]
fn Constructor(
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
transformer: Option<*mut JSObject>,
writable_strategy: &QueuingStrategy,
readable_strategy: &QueuingStrategy,
) -> Fallible<DomRoot<TransformStream>> {
// If transformer is missing, set it to null.
rooted!(in(*cx) let transformer_obj = transformer.unwrap_or(ptr::null_mut()));
rooted!(&in(cx) let transformer_obj = transformer.unwrap_or(ptr::null_mut()));
// Let underlyingSinkDict be underlyingSink,
// converted to an IDL value of type UnderlyingSink.
let transformer_dict = if !transformer_obj.is_null() {
rooted!(in(*cx) let obj_val = ObjectValue(transformer_obj.get()));
match Transformer::new(cx, obj_val.handle(), can_gc) {
rooted!(&in(cx) let obj_val = ObjectValue(transformer_obj.get()));
match Transformer::new(cx.into(), obj_val.handle(), CanGc::from_cx(cx)) {
Ok(ConversionResult::Success(val)) => val,
Ok(ConversionResult::Failure(error)) => {
return Err(Error::Type(error.into_owned()));
@@ -1004,20 +1002,20 @@ impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
let readable_high_water_mark = extract_high_water_mark(readable_strategy, 0.0)?;
// Let readableSizeAlgorithm be ! ExtractSizeAlgorithm(readableStrategy).
let readable_size_algorithm = extract_size_algorithm(readable_strategy, can_gc);
let readable_size_algorithm = extract_size_algorithm(readable_strategy, CanGc::from_cx(cx));
// Let writableHighWaterMark be ? ExtractHighWaterMark(writableStrategy, 1).
let writable_high_water_mark = extract_high_water_mark(writable_strategy, 1.0)?;
// Let writableSizeAlgorithm be ! ExtractSizeAlgorithm(writableStrategy).
let writable_size_algorithm = extract_size_algorithm(writable_strategy, can_gc);
let writable_size_algorithm = extract_size_algorithm(writable_strategy, CanGc::from_cx(cx));
// Let startPromise be a new promise.
let start_promise = Promise::new(global, can_gc);
let start_promise = Promise::new2(cx, global);
// Perform ! InitializeTransformStream(this, startPromise, writableHighWaterMark,
// writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm).
let stream = TransformStream::new_with_proto(global, proto, can_gc);
let stream = TransformStream::new_with_proto(global, proto, CanGc::from_cx(cx));
stream.initialize(
cx,
global,
@@ -1026,7 +1024,6 @@ impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
writable_size_algorithm,
readable_high_water_mark,
readable_size_algorithm,
can_gc,
)?;
// Perform ? SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict).
@@ -1034,22 +1031,22 @@ impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
global,
transformer_obj.handle(),
&transformer_dict,
can_gc,
CanGc::from_cx(cx),
);
// If transformerDict["start"] exists, then resolve startPromise with the
// result of invoking transformerDict["start"]
// with argument list « this.[[controller]] » and callback this value transformer.
if let Some(start) = &transformer_dict.start {
rooted!(in(*cx) let mut result_object = ptr::null_mut::<JSObject>());
rooted!(in(*cx) let mut result: JSVal);
rooted!(in(*cx) let this_object = transformer_obj.get());
rooted!(&in(cx) let mut result_object = ptr::null_mut::<JSObject>());
rooted!(&in(cx) let mut result: JSVal);
rooted!(&in(cx) let this_object = transformer_obj.get());
start.Call_(
&this_object.handle(),
&stream.get_controller(),
result.handle_mut(),
ExceptionHandling::Rethrow,
can_gc,
CanGc::from_cx(cx),
)?;
let is_promise = unsafe {
if result.is_object() {
@@ -1060,14 +1057,14 @@ impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
}
};
let promise = if is_promise {
Promise::new_with_js_promise(result_object.handle(), cx)
Promise::new_with_js_promise(result_object.handle(), cx.into())
} else {
Promise::new_resolved(global, cx, result.get(), can_gc)
Promise::new_resolved(global, cx.into(), result.get(), CanGc::from_cx(cx))
};
start_promise.resolve_native(&promise, can_gc);
start_promise.resolve_native(&promise, CanGc::from_cx(cx));
} else {
// Otherwise, resolve startPromise with undefined.
start_promise.resolve_native(&(), can_gc);
start_promise.resolve_native(&(), CanGc::from_cx(cx));
};
Ok(stream)

View File

@@ -914,21 +914,20 @@ impl WritableStream {
// Perform ! SetUpWritableStreamDefaultController
controller
.setup(cx.into(), &global, self, CanGc::from_cx(cx))
.setup(cx, &global, self)
.expect("Setup for transfer cannot fail");
}
/// <https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink>
#[allow(clippy::too_many_arguments)]
pub(crate) fn setup_from_underlying_sink(
fn setup_from_underlying_sink(
&self,
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
stream: &WritableStream,
underlying_sink_obj: SafeHandleObject,
underlying_sink: &UnderlyingSink,
strategy_hwm: f64,
strategy_size: Rc<QueuingStrategySize>,
can_gc: CanGc,
) -> Result<(), Error> {
// Let controller be a new WritableStreamDefaultController.
@@ -965,7 +964,7 @@ impl WritableStream {
),
strategy_hwm,
strategy_size,
can_gc,
CanGc::from_cx(cx),
);
// Note: this must be done before `setup`,
@@ -973,26 +972,25 @@ impl WritableStream {
controller.set_underlying_sink_this_object(underlying_sink_obj);
// Perform ? SetUpWritableStreamDefaultController
controller.setup(cx, global, stream, can_gc)
controller.setup(cx, global, stream)
}
}
/// <https://streams.spec.whatwg.org/#create-writable-stream>
#[cfg_attr(crown, expect(crown::unrooted_must_root))]
pub(crate) fn create_writable_stream(
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
writable_high_water_mark: f64,
writable_size_algorithm: Rc<QueuingStrategySize>,
underlying_sink_type: UnderlyingSinkType,
can_gc: CanGc,
) -> Fallible<DomRoot<WritableStream>> {
// Assert: ! IsNonNegativeNumber(highWaterMark) is true.
assert!(writable_high_water_mark >= 0.0);
// Let stream be a new WritableStream.
// Perform ! InitializeWritableStream(stream).
let stream = WritableStream::new_with_proto(global, None, can_gc);
let stream = WritableStream::new_with_proto(global, None, CanGc::from_cx(cx));
// Let controller be a new WritableStreamDefaultController.
let controller = WritableStreamDefaultController::new(
@@ -1000,12 +998,12 @@ pub(crate) fn create_writable_stream(
underlying_sink_type,
writable_high_water_mark,
writable_size_algorithm,
can_gc,
CanGc::from_cx(cx),
);
// Perform ? SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm,
// closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm).
controller.setup(cx, global, &stream, can_gc)?;
controller.setup(cx, global, &stream)?;
// Return stream.
Ok(stream)
@@ -1014,21 +1012,20 @@ pub(crate) fn create_writable_stream(
impl WritableStreamMethods<crate::DomTypeHolder> for WritableStream {
/// <https://streams.spec.whatwg.org/#ws-constructor>
fn Constructor(
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
underlying_sink: Option<*mut JSObject>,
strategy: &QueuingStrategy,
) -> Fallible<DomRoot<WritableStream>> {
// If underlyingSink is missing, set it to null.
rooted!(in(*cx) let underlying_sink_obj = underlying_sink.unwrap_or(ptr::null_mut()));
rooted!(&in(cx) let underlying_sink_obj = underlying_sink.unwrap_or(ptr::null_mut()));
// Let underlyingSinkDict be underlyingSink,
// converted to an IDL value of type UnderlyingSink.
let underlying_sink_dict = if !underlying_sink_obj.is_null() {
rooted!(in(*cx) let obj_val = ObjectValue(underlying_sink_obj.get()));
match UnderlyingSink::new(cx, obj_val.handle(), can_gc) {
rooted!(&in(cx) let obj_val = ObjectValue(underlying_sink_obj.get()));
match UnderlyingSink::new(cx.into(), obj_val.handle(), CanGc::from_cx(cx)) {
Ok(ConversionResult::Success(val)) => val,
Ok(ConversionResult::Failure(error)) => {
return Err(Error::Type(error.into_owned()));
@@ -1047,10 +1044,10 @@ impl WritableStreamMethods<crate::DomTypeHolder> for WritableStream {
}
// Perform ! InitializeWritableStream(this).
let stream = WritableStream::new_with_proto(global, proto, can_gc);
let stream = WritableStream::new_with_proto(global, proto, CanGc::from_cx(cx));
// Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy).
let size_algorithm = extract_size_algorithm(strategy, can_gc);
let size_algorithm = extract_size_algorithm(strategy, CanGc::from_cx(cx));
// Let highWaterMark be ? ExtractHighWaterMark(strategy, 1).
let high_water_mark = extract_high_water_mark(strategy, 1.0)?;
@@ -1065,7 +1062,6 @@ impl WritableStreamMethods<crate::DomTypeHolder> for WritableStream {
&underlying_sink_dict,
high_water_mark,
size_algorithm,
can_gc,
)?;
Ok(stream)

View File

@@ -29,7 +29,7 @@ use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::dom::readablestreamdefaultcontroller::{EnqueuedValue, QueueWithSizes, ValueWithSize};
use crate::dom::stream::writablestream::WritableStream;
use crate::dom::types::{AbortController, AbortSignal, TransformStream};
use crate::realms::{InRealm, enter_auto_realm, enter_realm};
use crate::realms::{InRealm, enter_auto_realm};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
impl js::gc::Rootable for CloseAlgorithmFulfillmentHandler {}
@@ -442,10 +442,9 @@ impl WritableStreamDefaultController {
/// <https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller>
pub(crate) fn setup(
&self,
cx: SafeJSContext,
cx: &mut js::context::JSContext,
global: &GlobalScope,
stream: &WritableStream,
can_gc: CanGc,
) -> Result<(), Error> {
// Assert: stream implements WritableStream.
// Implied by stream type.
@@ -481,21 +480,21 @@ impl WritableStreamDefaultController {
let backpressure = self.get_backpressure();
// Perform ! WritableStreamUpdateBackpressure(stream, backpressure).
stream.update_backpressure(backpressure, global, can_gc);
stream.update_backpressure(backpressure, global, CanGc::from_cx(cx));
// Let startResult be the result of performing startAlgorithm. (This may throw an exception.)
// Let startPromise be a promise resolved with startResult.
let start_promise = self.start_algorithm(cx, global, can_gc)?;
let start_promise = self.start_algorithm(cx.into(), global, CanGc::from_cx(cx))?;
let rooted_default_controller = DomRoot::from_ref(self);
// Upon fulfillment of startPromise,
rooted!(in(*cx) let mut fulfillment_handler = Some(StartAlgorithmFulfillmentHandler {
rooted!(&in(cx) let mut fulfillment_handler = Some(StartAlgorithmFulfillmentHandler {
controller: Dom::from_ref(&rooted_default_controller),
}));
// Upon rejection of startPromise with reason r,
rooted!(in(*cx) let mut rejection_handler = Some(StartAlgorithmRejectionHandler {
rooted!(&in(cx) let mut rejection_handler = Some(StartAlgorithmRejectionHandler {
controller: Dom::from_ref(&rooted_default_controller),
}));
@@ -503,11 +502,14 @@ impl WritableStreamDefaultController {
global,
fulfillment_handler.take().map(|h| Box::new(h) as Box<_>),
rejection_handler.take().map(|h| Box::new(h) as Box<_>),
can_gc,
CanGc::from_cx(cx),
);
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
start_promise.append_native_handler(&handler, comp, can_gc);
let mut realm = enter_auto_realm(cx, global);
let cx = &mut realm.current_realm();
let in_realm_proof = cx.into();
let comp = InRealm::Already(&in_realm_proof);
start_promise.append_native_handler(&handler, comp, CanGc::from_cx(cx));
Ok(())
}

View File

@@ -114,6 +114,10 @@ DOMInterfaces = {
'cx': ['Constructor']
},
'CompressionStream': {
'cx': ['Constructor'],
},
'CookieStore': {
'cx': ['Set', 'Set_', 'Get', 'Get_', 'GetAll', 'GetAll_', 'Delete', 'Delete_'],
},
@@ -197,6 +201,10 @@ DOMInterfaces = {
'useSystemCompartment': True,
},
'DecompressionStream': {
'cx': ['Constructor'],
},
'DedicatedWorkerGlobalScope': {
'cx': ['PostMessage', 'PostMessage_'],
},
@@ -877,10 +885,22 @@ DOMInterfaces = {
'cx_no_gc': ['WholeText']
},
'TextDecoderStream': {
'cx': ['Constructor'],
},
'TextEncoder': {
'canGc': ['Encode']
},
'TextEncoderStream': {
'cx': ['Constructor'],
},
'TransformStream': {
'cx': ['Constructor'],
},
'TreeWalker': {
'cx': ['ParentNode', 'PreviousNode', 'NextNode', 'FirstChild', 'LastChild', 'PreviousSibling', 'NextSibling']
},
@@ -1073,6 +1093,7 @@ DOMInterfaces = {
'WritableStream': {
'canGc': ['Close', 'GetWriter'],
'cx': ['Constructor'],
'inRealms': ['GetWriter'],
'realm': ['Abort', 'Close']
},

View File

@@ -201,6 +201,63 @@ fn test_accessibility_after_navigate_and_back() {
// TODO(accessibility): write test for resend a11y tree when clicking back or forward
#[test]
fn test_accessibility_basic_mapping() {
let servo_test = ServoTest::new_with_builder(|builder| {
let mut preferences = Preferences::default();
preferences.accessibility_enabled = true;
builder.preferences(preferences)
});
let delegate = Rc::new(WebViewDelegateImpl::default());
let mut element_role_pairs = VecDeque::from([
("article", Role::Article),
("aside", Role::Complementary),
("footer", Role::ContentInfo),
("h1", Role::Heading),
("h2", Role::Heading),
("h3", Role::Heading),
("h4", Role::Heading),
("h5", Role::Heading),
("h6", Role::Heading),
("header", Role::Banner),
("hr", Role::Splitter),
("main", Role::Main),
("nav", Role::Navigation),
("p", Role::Paragraph),
]);
let mut url: String = "data:text/html,<!DOCTYPE html>".to_owned();
for (element, _) in element_role_pairs.iter() {
url.push_str(format!("<{element}></{element}>").as_str());
}
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse(url.as_str()).unwrap())
.build();
webview.set_accessibility_active(true);
let load_webview = webview.clone();
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let tree = build_tree(updates);
let root = assert_tree_structure_and_get_root_web_area(&tree);
assert_eq!(root.children().len(), element_role_pairs.len());
for child in root.children() {
let Some((tag, role)) = element_role_pairs.pop_front() else {
panic!("Number of children of root node should match number of tag/role pairs");
};
assert_eq!(child.data().html_tag(), Some(tag));
assert_eq!(child.role(), role);
}
assert!(
element_role_pairs.is_empty(),
"Number of children of root node should match number of tag/role pairs"
);
}
fn wait_for_min_updates(
servo_test: &ServoTest,
delegate: Rc<WebViewDelegateImpl>,
@@ -266,13 +323,8 @@ fn assert_tree_structure_and_get_root_web_area<'tree>(
let graft_node = scroll_view_children[0];
assert!(graft_node.is_graft());
let graft_node_children: Vec<accesskit_consumer::Node<'_>> = graft_node.children().collect();
assert_eq!(graft_node_children.len(), 1);
let root_web_area = graft_node_children[0];
assert_eq!(root_web_area.role(), Role::RootWebArea);
root_web_area
find_first_matching_node(graft_node, |node| node.role() == Role::RootWebArea)
.expect("Should have a RootWebArea")
}
fn find_first_matching_node(