mod common;
use std::cell::Cell;
use std::collections::VecDeque;
use std::rc::Rc;
use accesskit::Role::{self, GenericContainer};
use accesskit::{Action, ActionRequest, Affine, NodeId, Rect, TreeId, TreeUpdate};
use accesskit_consumer::TreeChangeHandler;
use euclid::Scale;
use servo::{
DiagnosticsLoggingOption, LoadStatus, Opts, Preferences, Scroll, WebView, WebViewBuilder,
WebViewPoint, WebViewVector,
};
use url::Url;
use webrender_api::units::{DevicePoint, DeviceVector2D};
use crate::common::{ServoTest, WebViewDelegateImpl, evaluate_javascript};
struct NoOpChangeHandler;
impl TreeChangeHandler for NoOpChangeHandler {
fn node_added(&mut self, _: &accesskit_consumer::Node) {}
fn node_updated(&mut self, _: &accesskit_consumer::Node, _: &accesskit_consumer::Node) {}
fn focus_moved(
&mut self,
_: Option<&accesskit_consumer::Node>,
_: Option<&accesskit_consumer::Node>,
) {
}
fn node_removed(&mut self, _: &accesskit_consumer::Node) {}
}
#[test]
fn test_basic_accessibility_update() {
let servo_test = build_test();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,<!DOCTYPE html>").unwrap())
.build();
webview.set_accessibility_active(true);
servo_test.spin(|| webview.load_status() != LoadStatus::Complete);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let tree = build_tree(updates);
let _ = assert_tree_structure_and_get_root_web_area(&tree);
}
#[test]
fn test_activate_accessibility_after_layout() {
let servo_test = build_test();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,<!DOCTYPE html>").unwrap())
.build();
servo_test.spin(|| webview.load_status() != LoadStatus::Complete);
webview.set_accessibility_active(true);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let tree = build_tree(updates);
let _ = assert_tree_structure_and_get_root_web_area(&tree);
}
#[test]
fn test_navigate_creates_new_accessibility_update() {
let servo_test = build_test();
let page_1_url = Url::parse("data:text/html,<!DOCTYPE html> page 1").unwrap();
let page_2_url = Url::parse("data:text/html,<!DOCTYPE html> page 2").unwrap();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(page_1_url)
.build();
webview.set_accessibility_active(true);
servo_test.spin(|| webview.load_status() != LoadStatus::Complete);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let mut tree = build_tree(updates);
let root_web_area = assert_tree_structure_and_get_root_web_area(&tree);
let result = find_first_matching_node(root_web_area, |node| {
node.role() == accesskit::Role::TextRun
});
let text_node = result.expect("Should be exactly one TextRun in the tree");
assert_eq!(text_node.value().as_deref(), Some("page 1"));
webview.load(page_2_url.clone());
servo_test.spin(|| webview.url() != Some(page_2_url.clone()));
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
for tree_update in new_updates {
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
}
let root_node = tree.state().root();
let result =
find_first_matching_node(root_node, |node| node.role() == accesskit::Role::TextRun);
let text_node = result.expect("Should be exactly one TextRun in the tree");
assert_eq!(text_node.value().as_deref(), Some("page 2"));
}
#[test]
fn test_accessibility_after_navigate_and_back() {
let servo_test = build_test();
let page_1_url = Url::parse("data:text/html,<!DOCTYPE html> page 1").unwrap();
let page_2_url = Url::parse("data:text/html,<!DOCTYPE html> page 2").unwrap();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(page_1_url.clone())
.build();
webview.set_accessibility_active(true);
servo_test.spin(|| webview.load_status() != LoadStatus::Complete);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let mut tree = build_tree(updates);
let root_web_area = assert_tree_structure_and_get_root_web_area(&tree);
let result = find_all_matching_nodes(root_web_area, |node| {
node.role() == accesskit::Role::TextRun
});
assert_eq!(result.len(), 1);
let text_node = result[0];
assert_eq!(text_node.value().as_deref(), Some("page 1"));
webview.load(page_2_url.clone());
servo_test.spin(|| webview.url() != Some(page_2_url.clone()));
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
for tree_update in new_updates {
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
}
let root_node = tree.state().root();
let result = find_all_matching_nodes(root_node, |node| node.role() == accesskit::Role::TextRun);
assert_eq!(result.len(), 1);
let text_node = result[0];
assert_eq!(text_node.value().as_deref(), Some("page 2"));
webview.go_back(1);
servo_test.spin(|| webview.url() != Some(page_1_url.clone()));
let new_updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
for tree_update in new_updates {
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
}
let root_node = tree.state().root();
let result = find_all_matching_nodes(root_node, |node| node.role() == accesskit::Role::TextRun);
assert_eq!(result.len(), 1);
let text_node = result[0];
assert_eq!(text_node.value().as_deref(), Some("page 1"));
}
#[test]
fn test_accessibility_basic_mapping() {
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 (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url.as_str());
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"
);
}
#[test]
fn test_accessibility_basic_role_from_attribute() {
let url = "data:text/html,<!DOCTYPE html><div role='blockquote'></div>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let first_child = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_eq!(first_child.role(), Role::Blockquote);
}
#[test]
fn test_accessibility_basic_name_from_contents() {
let url = "data:text/html,<!DOCTYPE html><h1>Servo</h1>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let first_child = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_eq!(first_child.role(), Role::Heading);
assert_eq!(first_child.label(), Some("Servo".to_owned()));
}
#[test]
fn test_accessibility_name_from_contents_subtree() {
let url = "data:text/html,<!DOCTYPE html>\
<h1>Servo aims to empower <code>developers</code> with a <em>lightweight</em>, \
<strong>high-performance</strong> alternative for <span>embedding \
<span>web technologies</span> in <span>applications</span></span>.</h1>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_eq!(heading.role(), Role::Heading);
let heading_children: Vec<accesskit_consumer::Node> = heading.children().collect();
assert_eq!(heading_children.len(), 9);
assert_eq!(
heading.label(),
Some(
"Servo aims to empower developers with a lightweight, high-performance alternative for \
embedding web technologies in applications."
.to_owned()
),
"Heading label should be composed of the text contents of all of its descendant text nodes"
);
}
#[test]
fn test_accessibility_basic_mutation() {
let url = "data:text/html,<!DOCTYPE html>\
<h1 id='h1'>This is an h1</h1>\
<h2 id='h2'>This is an h2</h2>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<_> = root.children().collect();
assert_eq!(children.len(), 2);
let h1 = children[0];
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
let h2 = children[1];
assert_eq!(h2.label(), Some("This is an h2".to_owned()));
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.getElementById('h2').remove();",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
assert_eq!(update.nodes.len(), 2);
let root_web_area = find_node_with_role(&update, Role::RootWebArea);
assert_eq!(root_web_area.children().len(), 1);
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<_> = root.children().collect();
assert_eq!(children.len(), 1);
let h1 = children[0];
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
}
#[test]
fn test_accessibility_with_mutation_move_nodes() {
let url = "data:text/html,<!DOCTYPE html>\
<div id='div1'></div>\
<h1 id='h1'>This is an h1</h1>\
<h2 id='h2'>This is an h2</h2>\
<div id='div2'></div>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 4);
let div1 = children[0];
let h1 = children[1];
let h2 = children[2];
let div2 = children[3];
assert_eq!(div1.role(), Role::GenericContainer);
assert_eq!(h1.role(), Role::Heading);
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
assert_eq!(h2.role(), Role::Heading);
assert_eq!(h2.label(), Some("This is an h2".to_owned()));
assert_eq!(div2.role(), Role::GenericContainer);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"div1.moveBefore(h1,null); div2.appendChild(h2);",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
assert_eq!(update.nodes.len(), 3);
assert_eq!(update.nodes[0].1.role(), Role::GenericContainer);
assert_eq!(update.nodes[0].1.children().len(), 1);
assert_eq!(update.nodes[1].1.role(), Role::GenericContainer);
assert_eq!(update.nodes[1].1.children().len(), 1);
assert_eq!(update.nodes[2].1.role(), Role::RootWebArea);
assert_eq!(update.nodes[2].1.children().len(), 2);
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
assert_eq!(root.children().count(), 2);
let div1 = root.children().nth(0).unwrap();
let div2 = root.children().nth(1).unwrap();
assert_eq!(div1.children().count(), 1);
assert_eq!(div2.children().count(), 1);
let h1 = div1.children().nth(0).unwrap();
let h2 = div2.children().nth(0).unwrap();
assert_eq!(h1.role(), Role::Heading);
assert_eq!(h1.label().as_deref(), Some("This is an h1"));
assert_eq!(h2.role(), Role::Heading);
assert_eq!(h2.label().as_deref(), Some("This is an h2"));
}
#[test]
fn test_accessibility_text_change() {
let url = "data:text/html,<!DOCTYPE html>\
<h1 id='h1'>This is an h1</h1>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let h1 = children[0];
assert_eq!(h1.role(), Role::Heading);
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"h1.firstChild.appendData(', now with more text');",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
let _ = find_node_with_role(&update, Role::TextRun);
let heading = find_node_with_role(&update, Role::Heading);
assert_eq!(heading.children().len(), 1);
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let h1 = children[0];
assert_eq!(h1.role(), Role::Heading);
assert_eq!(
h1.label(),
Some("This is an h1, now with more text".to_owned())
);
}
#[test]
fn test_accessibility_role_mutations() {
let url = "data:text/html,<!DOCTYPE html>\
<div id='div1' role='emphasis'>Servo</div>\
<aside id='aside'>Aside</aside>\
<main id='main' role='status'>Main</main>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 3);
let div = children[0];
assert_eq!(
div.role(),
Role::Emphasis,
"<div> should respect author's override"
);
let aside = children[1];
assert_eq!(
aside.role(),
Role::Complementary,
"<aside> should default to implicit ARIA role"
);
let main = children[2];
assert_eq!(
main.role(),
Role::Status,
"<main> should respect author's override"
);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"\
div1.role = 'blockquote';\
aside.role = 'presentation';\
main.role = null;\
",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
assert_eq!(update.nodes.len(), 3);
assert_eq!(
update.nodes[0].1.role(),
Role::Main,
"<main> should fallback to implicit role when explicit role is removed"
);
assert_eq!(
update.nodes[1].1.role(),
Role::Blockquote,
"<div> should have the new explicit role"
);
assert_eq!(
update.nodes[2].1.role(),
Role::GenericContainer,
"<aside> should use the explicit role instead of the implicit role"
);
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 3);
assert_eq!(children[0].role(), Role::Blockquote);
assert_eq!(children[1].role(), Role::GenericContainer);
assert_eq!(children[2].role(), Role::Main);
}
#[test]
fn test_accessibility_partial_subtree_move_and_delete() {
let url = "data:text/html,<!DOCTYPE html>\
<header id='header'>\
<div id='div'>\
<h1 id='h1'>This is an h1</h1>\
<p id='p'>This is a paragraph</p>\
</div>\
</header>\
<article id='article'><h2 id='h2'>This is an h2</h2></article>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 2);
let header = children[0];
assert_eq!(header.role(), Role::Banner);
let h1 = find_all_matching_nodes(header, |node| node.role() == Role::Heading)[0];
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
let _p = find_all_matching_nodes(header, |node| node.role() == Role::Paragraph)
.pop()
.expect("Should be exactly one Paragraph node");
let article = children[1];
assert_eq!(article.role(), Role::Article);
let h2 = find_all_matching_nodes(article, |node| node.role() == Role::Heading)[0];
assert_eq!(h2.label(), Some("This is an h2".to_owned()));
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"article.moveBefore(div, h2);\
article.moveBefore(h1, div);\
p.remove();\
header.remove();",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let article = children[0];
assert_eq!(article.role(), Role::Article);
let children: Vec<accesskit_consumer::Node> = article.children().collect();
assert_eq!(children.len(), 3);
let h1 = children[0];
assert_eq!(h1.role(), Role::Heading);
assert_eq!(h1.label(), Some("This is an h1".to_owned()));
let div = children[1];
assert_eq!(div.role(), Role::GenericContainer);
let h2 = children[2];
assert_eq!(h2.role(), Role::Heading);
assert_eq!(h2.label(), Some("This is an h2".to_owned()));
}
#[test]
fn test_accessibility_children_of_heading_change() {
let url = "data:text/html,<!DOCTYPE html>\
<h2>Activating accessibility for a <code>WebView</code></h2>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let heading = children[0];
assert_eq!(heading.role(), Role::Heading);
assert_eq!(
heading.label(),
Some("Activating accessibility for a WebView".to_owned())
);
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 2);
assert_eq!(heading_children[0].role(), Role::TextRun);
assert_eq!(heading_children[1].role(), Role::GenericContainer);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('code').remove();",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Heading should still be in the tree");
assert_eq!(
heading.label(),
Some("Activating accessibility for a".to_owned())
);
}
#[test]
fn test_accessibility_descendants_of_heading_change() {
let url = "data:text/html,<!DOCTYPE html>\
<h1>We really <em>really <strong>really</strong></em> like owls</h1>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let heading = children[0];
assert_eq!(heading.role(), Role::Heading);
assert_eq!(
heading.label(),
Some("We really really really like owls".to_owned())
);
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 3);
assert_eq!(heading_children[0].role(), Role::TextRun);
assert_eq!(heading_children[1].role(), Role::GenericContainer);
assert_eq!(heading_children[2].role(), Role::TextRun);
let em = heading_children[1];
assert_eq!(em.children().collect::<Vec<_>>().len(), 2);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('strong').remove();",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Heading should still be in the tree");
assert_eq!(
heading.label(),
Some("We really really like owls".to_owned())
);
}
#[test]
fn test_accessibility_bounds() {
let url = "data:text/html,<!DOCTYPE html>\
<div id='box' style='position:absolute;left:10px;top:20px;\
width:100px;height:50px'></div>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let scroll_view = find_first_matching_node(tree.state().root(), |node| {
node.role() == Role::ScrollView
})
.expect(
"Tree should include a scroll view corresponding to the WebView. (covers whole viewport)",
);
assert_rect_eq(
scroll_view
.raw_bounds()
.expect("WebView node should have bounds"),
Rect::new(0.0, 0.0, TEST_VIEWPORT_SIZE, TEST_VIEWPORT_SIZE),
);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let div = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_eq!(div.data().html_tag(), Some("div"));
let expected = Rect::new(10.0, 20.0, 110.0, 70.0);
assert_rect_eq(div.raw_bounds().expect("div should have bounds"), expected);
assert_rect_eq(
div.bounding_box().expect("div should have a bounding box"),
expected,
);
}
#[test]
fn test_accessibility_webview_bounds_updated_after_hidpi_change() {
let (servo_test, delegate, webview, mut tree) =
build_webview_and_tree("data:text/html,<!DOCTYPE html>");
webview.set_hidpi_scale_factor(Scale::new(2.0));
for update in wait_for_min_updates(&servo_test, delegate, 1) {
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
}
let scroll_view =
find_first_matching_node(tree.state().root(), |node| node.role() == Role::ScrollView)
.expect("Tree should include a scroll view corresponding to the WebView");
assert_rect_eq(
scroll_view
.raw_bounds()
.expect("WebView node should have bounds"),
Rect::new(0.0, 0.0, TEST_VIEWPORT_SIZE / 2.0, TEST_VIEWPORT_SIZE / 2.0),
);
}
#[test]
fn test_accessibility_text_run_bounds() {
let url = "data:text/html,<!DOCTYPE html>\
<h1 style='position:absolute;left:0;top:0;margin:0;\
width:300px;height:40px'>Servo</h1>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_eq!(heading.role(), Role::Heading);
assert_rect_eq(
heading.raw_bounds().expect("heading should have bounds"),
Rect::new(0.0, 0.0, 300.0, 40.0),
);
let text_run = find_first_matching_node(heading, |node| node.role() == Role::TextRun)
.expect("Heading should contain a TextRun");
assert!(
text_run.raw_bounds().is_none(),
"TextRun bounds are not implemented yet, but got {:?}",
text_run.raw_bounds()
);
}
#[test]
fn test_accessibility_bounds_omitted_for_display_contents() {
let url = "data:text/html,<!DOCTYPE html>\
<div style='position:absolute;left:0;top:0;width:200px;height:30px'>\
<span style='display:contents'>hello</span></div>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let div = root
.children()
.next()
.expect("Root web area should have at least one child.");
assert_rect_eq(
div.raw_bounds().expect("div should have bounds"),
Rect::new(0.0, 0.0, 200.0, 30.0),
);
let span = find_first_matching_node(root, |node| node.data().html_tag() == Some("span"))
.expect("Document should contain the `display: contents` span");
assert!(
span.raw_bounds().is_none(),
"`display: contents` bounds are not implemented yet, but got {:?}",
span.raw_bounds()
);
let text_run = find_first_matching_node(span, |node| node.role() == Role::TextRun)
.expect("The `display: contents` span should contain a TextRun");
assert!(
text_run.raw_bounds().is_none(),
"TextRun bounds are not implemented yet, but got {:?}",
text_run.raw_bounds()
);
}
#[test]
fn test_accessibility_bounds_omitted_for_display_none() {
let url = "data:text/html,<!DOCTYPE html>\
<div style='position:absolute;left:0;top:0;width:200px;height:30px'>\
<span id='hidden' style='display:none'>hidden</span></div>";
let (_servo_test, _delegate, _webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
for node in find_all_matching_nodes(root, |node| {
node.data().html_tag() == Some("span") || node.role() == Role::TextRun
}) {
assert!(
node.raw_bounds().is_none(),
"`display: none` content should have no bounds, but {:?} had {:?}",
node.role(),
node.raw_bounds()
);
}
}
#[test]
fn test_accessibility_bounds_updated_after_relayout() {
let url = "data:text/html,<!DOCTYPE html>\
<div id='box' style='position:absolute;left:10px;top:20px;\
width:100px;height:50px'></div>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let div = root.children().next().expect("Should have a div child");
let div_id = div.locate().0; assert_rect_eq(
div.raw_bounds().expect("div should have bounds"),
Rect::new(10.0, 20.0, 110.0, 70.0),
);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"const box = document.getElementById('box');\
box.style.left = '30px'; box.style.width = '200px';",
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let expected = Rect::new(30.0, 20.0, 230.0, 70.0);
let updated_bounds = updates
.iter()
.flat_map(|update| update.nodes.iter())
.filter(|(id, _)| *id == div_id)
.filter_map(|(_, node)| node.bounds())
.next_back()
.expect("The div should have been re-sent with new bounds");
assert_rect_eq(updated_bounds, expected);
for update in updates {
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
}
let root = assert_tree_structure_and_get_root_web_area(&tree);
let div = root.children().next().expect("Should have a div child");
assert_rect_eq(div.raw_bounds().expect("div should have bounds"), expected);
}
#[test]
fn test_accessibility_bounds_updated_after_renderer_scroll() {
let url = "data:text/html,<!DOCTYPE html>\
<main style='position:absolute;left:10px;top:100px;\
width:100px;height:50px'>Target</main>\
<div style='width:2000px;height:2000px'></div>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
assert_rect_eq(
main.raw_bounds().expect("main should have bounds"),
Rect::new(10.0, 100.0, 110.0, 150.0),
);
webview.notify_scroll_event(
Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(20.0, 40.0))),
WebViewPoint::Device(DevicePoint::new(250.0, 250.0)),
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
for update in updates {
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
}
let root = assert_tree_structure_and_get_root_web_area(&tree);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
let expected = Rect::new(-10.0, 60.0, 90.0, 110.0);
assert_rect_eq(
main.bounding_box().expect("main should have bounds"),
expected,
);
}
#[test]
fn test_accessibility_bounds_updated_after_script_scroll() {
let url = "data:text/html,<!DOCTYPE html>\
<main style='position:absolute;left:10px;top:100px;\
width:100px;height:50px'>Target</main>\
<div style='width:2000px;height:2000px'></div>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
assert_rect_eq(
main.raw_bounds().expect("main should have bounds"),
Rect::new(10.0, 100.0, 110.0, 150.0),
);
let _ = evaluate_javascript(&servo_test, webview.clone(), "window.scrollTo(20, 40);");
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
for update in updates {
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
}
let root = assert_tree_structure_and_get_root_web_area(&tree);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
let expected = Rect::new(-10.0, 60.0, 90.0, 110.0);
assert_rect_eq(
main.bounding_box().expect("main should have bounds"),
expected,
);
}
#[test]
fn test_accessibility_build_initial_tree_after_scroll() {
let servo_test = build_test();
let url = "data:text/html,<!DOCTYPE html>\
<main style='position:absolute;left:10px;top:100px;\
width:100px;height:50px'>Target</main>\
<div style='width:2000px;height:2000px'></div>";
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse(url).unwrap())
.build();
servo_test.spin(|| webview.load_status() != LoadStatus::Complete);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
webview.notify_scroll_event(
Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(20.0, 40.0))),
WebViewPoint::Device(DevicePoint::new(250.0, 250.0)),
);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
webview.set_accessibility_active(true);
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);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
let expected = Rect::new(-10.0, 60.0, 90.0, 110.0);
assert_rect_eq(
main.bounding_box().expect("main should have bounds"),
expected,
);
}
#[test]
fn test_accessibility_unchanged_bounds_are_not_resent() {
let url = "data:text/html,<!DOCTYPE html>\
<section id='a' style='position:absolute;left:0;top:0;width:10px;height:10px'>\
<article id='c'></article>\
</section>\
<footer id='b' style='position:absolute;left:100px;top:100px;\
width:10px;height:10px'></footer>";
let (servo_test, delegate, webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 2);
let (node_a, node_b) = (children[0], children[1]);
assert_rect_eq(
node_a.raw_bounds().expect("a should have bounds"),
Rect::new(0.0, 0.0, 10.0, 10.0),
);
assert_rect_eq(
node_b.raw_bounds().expect("b should have bounds"),
Rect::new(100.0, 100.0, 110.0, 110.0),
);
let node_b_id = node_b.locate().0;
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.getElementById('a').style.width = '50px';",
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let resent_ids: Vec<NodeId> = updates
.iter()
.flat_map(|update| update.nodes.iter())
.map(|(id, _)| *id)
.collect();
assert!(
!resent_ids.contains(&node_b_id),
"A node whose bounds did not change should not be re-serialized, but got {resent_ids:?}"
);
}
#[test]
fn test_accessibility_bounds_are_computed_for_inline_elements() {
let url = "data:text/html,<!DOCTYPE html>\
<h1>We really <em>really <strong>really</strong></em> like owls</h1>";
let (_, _, _, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Should be exactly one heading");
assert_eq!(
heading.label(),
Some("We really really really like owls".to_owned())
);
assert!(heading.has_bounds());
let em = find_first_matching_node(heading, |node| node.role() == GenericContainer)
.expect("Heading should have one GenericContainer child");
assert!(em.has_bounds());
let strong = find_first_matching_node(em, |node| node.role() == GenericContainer)
.expect("<em> should have one GenericContainer child");
assert!(strong.has_bounds());
}
#[test]
fn test_accessibility_update_failed_layout_from_layout_root() {
let url = "data:text/html,<!DOCTYPE html>\
<section id='a' style='position:absolute;left:0;top:0;width:10px;height:10px'>\
<article id='b' style='position:absolute;top:20px;left:20px;\
width:20px;height:20px;'></article>\
</section>\
<footer id='c' style='position:absolute;left:100px;top:100px;\
width:10px;height:10px'></footer>";
let (servo_test, delegate, webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 2);
let (node_a, node_c) = (children[0], children[1]);
assert_rect_eq(
node_a.raw_bounds().expect("a should have bounds"),
Rect::new(0.0, 0.0, 10.0, 10.0),
);
assert_rect_eq(
node_c.raw_bounds().expect("c should have bounds"),
Rect::new(100.0, 100.0, 110.0, 110.0),
);
let node_a_id = node_a.locate().0;
let a_children: Vec<_> = node_a.children().collect();
let node_b = a_children[0];
assert_rect_eq(
node_b.raw_bounds().expect("b should have bounds"),
Rect::new(20.0, 20.0, 40.0, 40.0),
);
let node_b_id = node_b.locate().0;
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"b.style.position = 'fixed'; \
b.style.width = '30px'; \
a.style.width = '50px';",
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let update = &updates[0];
assert_eq!(update.nodes.len(), 2);
let node_a = find_node_matching(&update, |&id, _node| id == node_a_id);
let node_a_bounds = node_a.bounds().expect("a should have bounds after update");
assert_rect_eq(node_a_bounds, Rect::new(0.0, 0.0, 50.0, 10.0));
let node_b = find_node_matching(&update, |&id, _node| id == node_b_id);
let node_b_bounds = node_b.bounds().expect("b should have bounds after update");
assert_rect_eq(node_b_bounds, Rect::new(20.0, 20.0, 50.0, 40.0));
}
#[test]
fn test_accessibility_bounds_changed_by_sibling() {
let url = "data:text/html,<!DOCTYPE HTML>\
<body style='margin:0;'>\
<main id=main style='width:100px;height:100px;'></main>\
<footer id=footer style='width:100px;height:100px;'>Hello</footer>\
</body>";
let (servo_test, delegate, webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 2);
let (main, footer) = (children[0], children[1]);
assert_rect_eq(
main.raw_bounds().expect("main should have bounds"),
Rect::new(0.0, 0.0, 100.0, 100.0),
);
assert_rect_eq(
footer.raw_bounds().expect("footer should have bounds"),
Rect::new(0.0, 100.0, 100.0, 200.0),
);
let main_id = main.locate().0;
let footer_id = footer.locate().0;
let _ = evaluate_javascript(&servo_test, webview.clone(), "main.style.height = '200px';");
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let update = &updates[0];
let main = find_node_matching(&update, |&id, _node| id == main_id);
let main_bounds = main.bounds().expect("main should have bounds after update");
assert_rect_eq(main_bounds, Rect::new(0.0, 0.0, 100.0, 200.0));
let footer = find_node_matching(&update, |&id, _node| id == footer_id);
let footer_bounds = footer
.bounds()
.expect("footer should have bounds after update");
assert_rect_eq(footer_bounds, Rect::new(0.0, 200.0, 100.0, 300.0));
}
#[test]
fn test_accessibility_layout_root_node_also_changed() {
let url = "data:text/html,<!DOCTYPE html>\
<article id='a' style='margin:0;position:absolute;left:0;top:0;width:10px;height:10px'>\
<p id='c' style='margin:0;width:10px;height:10px'>c</p>\
</article>\
<footer id='b' style='position:absolute;left:100px;top:100px;\
width:10px;height:10px'></footer>";
let (servo_test, delegate, webview, tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 2);
let (node_a, node_b) = (children[0], children[1]);
assert_rect_eq(
node_a.raw_bounds().expect("a should have bounds"),
Rect::new(0.0, 0.0, 10.0, 10.0),
);
let node_a_id = node_a.locate().0;
assert_rect_eq(
node_b.raw_bounds().expect("b should have bounds"),
Rect::new(100.0, 100.0, 110.0, 110.0),
);
let node_a_children: Vec<accesskit_consumer::Node> = node_a.children().collect();
assert_eq!(node_a_children.len(), 1);
let node_c = node_a_children[0];
assert_rect_eq(
node_c.raw_bounds().expect("c should have bounds"),
Rect::new(0.0, 0.0, 10.0, 10.0),
);
let node_c_id = node_c.locate().0;
let js = "a.setAttribute('role', 'main'); \
c.style.width = '20px';";
let _ = evaluate_javascript(&servo_test, webview.clone(), js);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let update = updates[0].clone();
assert_eq!(update.nodes.len(), 2);
let node_a_update = find_node_matching(&update, |&id, _node| id == node_a_id);
assert_eq!(node_a_update.role(), Role::Main);
let node_c_update = find_node_matching(&update, |&id, _node| id == node_c_id);
let node_c_bounds = node_c_update
.bounds()
.expect("Node c should have bounds after update");
assert_rect_eq(node_c_bounds, Rect::new(0.0, 0.0, 20.0, 10.0));
}
#[test]
fn test_accessibility_display_none_change() {
let url = "data:text/html,<!DOCTYPE html>\
<style>section.subdued em { display: none }</style>
<section class='subdued'><h1>We <em>really</em> love the web</h1></section>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Should have a heading");
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 3);
let em = heading_children[1];
assert_eq!(em.is_hidden(), true);
assert_eq!(heading.label(), Some("We love the web".to_owned()));
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('section').removeAttribute('class');",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Heading should still be in the tree");
assert_eq!(heading.label(), Some("We really love the web".to_owned()));
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 3);
let em = heading_children[1];
assert_eq!(em.is_hidden(), false);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('section').className = 'subdued';\
document.querySelector('em').firstChild.appendData(', really');",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Heading should still be in the tree");
assert_eq!(heading.label(), Some("We love the web".to_owned()));
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 3);
let em = heading_children[1];
assert_eq!(em.is_hidden(), true);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('section').removeAttribute('class');",
);
let mut updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates.pop().expect("Guaranteed by assert above");
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let heading = find_first_matching_node(root, |node| node.role() == Role::Heading)
.expect("Heading should still be in the tree");
assert_eq!(
heading.label(),
Some("We really, really love the web".to_owned())
);
let heading_children: Vec<_> = heading.children().collect();
assert_eq!(heading_children.len(), 3);
let em = heading_children[1];
assert_eq!(em.is_hidden(), false);
}
#[test]
fn test_accessibility_display_none_change_scroll() {
let url = "data:text/html,<!DOCTYPE html>\
<body style='margin:0;'>\
<main id='main' style='width:1000px;height:100px;\
overflow:scroll'>\
<article style='width:1000px;height:50px;'></article>\
<div style='width:1000px;height:2000px'></div>\
<aside id='aside' style='width:1000px;height:50px;'></aside>\
</main>\
</body>";
let (servo_test, delegate, webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let main = find_first_matching_node(root, |node| node.role() == Role::Main)
.expect("Document should contain a main element");
assert_rect_eq(
main.bounding_box().expect("main should have bounds"),
Rect::new(0.0, 0.0, 1000.0, 100.0),
);
let article = find_first_matching_node(root, |node| node.role() == Role::Article)
.expect("Document should contain an article");
let article_id = article.locate().0;
assert_rect_eq(
article.bounding_box().expect("article should have bounds"),
Rect::new(0.0, 0.0, 1000.0, 50.0),
);
let div = find_first_matching_node(root, |node| node.role() == Role::GenericContainer)
.expect("Document should contain a div");
let div_id = div.locate().0;
let aside = find_first_matching_node(root, |node| node.role() == Role::Complementary)
.expect("Document should contain an aside");
assert_rect_eq(
aside.bounding_box().expect("aside should have bounds"),
Rect::new(0.0, 2050.0, 1000.0, 2100.0),
);
let aside_id = aside.locate().0;
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"aside.style.display = 'none';",
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
let update = updates[0].clone();
assert_eq!(update.nodes.len(), 1, "only <aside> should be updated");
let _ = find_node_matching(&update, |id, _node| id == &aside_id);
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let aside = find_first_matching_node(root, |node| node.role() == Role::Complementary)
.expect("Aside should stil be present");
assert_eq!(aside.is_hidden(), true);
assert_eq!(aside.bounding_box(), None);
let _ = evaluate_javascript(&servo_test, webview.clone(), "main.scrollTo(0, 500);");
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates[0].clone();
assert_eq!(
update.nodes.len(),
2,
"only <article> and <div> should be updated"
);
let transform = Affine::translate((0.0, -500.0));
let article_data = find_node_matching(&update, |id, _node| id == &article_id);
assert_eq!(article_data.transform(), Some(&transform));
let div_data = find_node_matching(&update, |id, _node| id == &div_id);
assert_eq!(div_data.transform(), Some(&transform));
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let article = find_first_matching_node(root, |node| node.role() == Role::Article)
.expect("Document should contain an article");
let article_id = article.locate().0;
assert_rect_eq(
article.bounding_box().expect("article should have bounds"),
Rect::new(0.0, -500.0, 1000.0, -450.0),
);
let _ = evaluate_javascript(
&servo_test,
webview.clone(),
"aside.style.removeProperty('display');",
);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 1);
let update = updates[0].clone();
assert_eq!(update.nodes.len(), 1, "only <aside> should be updated");
let aside_data = find_node_matching(&update, |id, _node| id == &aside_id);
assert_eq!(aside_data.transform(), Some(&transform));
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let aside = find_first_matching_node(root, |node| node.role() == Role::Complementary)
.expect("Aside should contain an aside");
let aside_id = aside.locate().0;
assert_eq!(aside.is_hidden(), false);
assert_rect_eq(
aside.bounding_box().expect("aside should have bounds"),
Rect::new(0.0, 1550.0, 1000.0, 1600.0),
);
}
#[test]
fn test_accessibility_click_link() {
let url = "data:text/html,<!DOCTYPE html>\
<a id='link' href='data:text/html,just%20a%20text%20node'>this is a link</a>";
let (servo_test, delegate, _webview, mut tree) = build_webview_and_tree(url);
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
assert_eq!(children.len(), 1);
let link = children[0];
assert_eq!(link.role(), Role::Link);
assert_eq!(link.label(), Some("this is a link".to_owned()));
let (target_node, target_tree) = link.locate();
let action_request = ActionRequest {
action: Action::Click,
target_tree,
target_node,
data: None,
};
servo_test
.servo
.forward_accessibility_action(action_request);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 1);
assert_eq!(updates.len(), 2);
for update in updates {
tree.update_and_process_changes(update, &mut NoOpChangeHandler);
}
let root = assert_tree_structure_and_get_root_web_area(&tree);
let children: Vec<accesskit_consumer::Node> = root.children().collect();
let text = children[0];
assert_eq!(text.role(), Role::TextRun);
assert_eq!(text.value(), Some("just a text node".to_owned()));
}
const TEST_VIEWPORT_SIZE: f64 = 500.0;
#[track_caller]
fn find_node_with_role(update: &TreeUpdate, role: Role) -> &accesskit::Node {
find_node_matching(update, |_, node| node.role() == role)
}
#[track_caller]
fn find_node_matching(
update: &TreeUpdate,
mut predicate: impl FnMut(&NodeId, &accesskit::Node) -> bool,
) -> &accesskit::Node {
let mut matches = update.nodes.iter().filter(|(id, node)| predicate(id, node));
let node = matches
.next()
.expect("Exactly one node should match predicate");
assert!(
matches.next().is_none(),
"Exactly one node should match predicate"
);
&node.1
}
#[track_caller]
fn assert_rect_eq(actual: Rect, expected: Rect) {
const EPSILON: f64 = 0.05;
assert!(
(actual.x0 - expected.x0).abs() < EPSILON &&
(actual.y0 - expected.y0).abs() < EPSILON &&
(actual.x1 - expected.x1).abs() < EPSILON &&
(actual.y1 - expected.y1).abs() < EPSILON,
"expected bounds {expected:?} but got {actual:?}"
);
}
fn build_test() -> ServoTest {
let servo_test = ServoTest::new_with_builder(|builder| {
let mut preferences = Preferences::default();
preferences.accessibility_enabled = true;
preferences.dom_servo_helpers_enabled = true;
preferences.expensive_accessibility_test_assertions_enabled = true;
let mut opts = Opts::default();
opts.debug
.toggle_option(DiagnosticsLoggingOption::AccessibilityTree, true);
builder.preferences(preferences).opts(opts)
});
servo_test
}
fn build_webview_and_tree(
url: &str,
) -> (
ServoTest,
Rc<WebViewDelegateImpl>,
servo::WebView,
accesskit_consumer::Tree,
) {
let servo_test = build_test();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse(url).unwrap())
.build();
webview.set_accessibility_active(true);
let load_webview = webview.clone();
servo_test.spin(|| load_webview.load_status() != LoadStatus::Complete);
let updates = wait_for_min_updates(&servo_test, delegate.clone(), 2);
let tree = build_tree(updates);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
(servo_test, delegate, webview, tree)
}
fn wait_for_min_updates(
servo_test: &ServoTest,
delegate: Rc<WebViewDelegateImpl>,
min_num_updates: usize,
) -> Vec<TreeUpdate> {
servo_test.spin(|| delegate.last_accesskit_tree_updates.borrow().len() < min_num_updates);
delegate
.last_accesskit_tree_updates
.borrow_mut()
.drain(..)
.collect()
}
fn build_tree(tree_updates: Vec<TreeUpdate>) -> accesskit_consumer::Tree {
let first_update = tree_updates[0].clone();
let tree_id = first_update.tree_id;
let root_node_id = NodeId(0x0);
let mut root_node = accesskit::Node::new(Role::GenericContainer);
let graft_node_id = NodeId(0x1);
let mut graft_node = accesskit::Node::new(Role::GenericContainer);
graft_node.set_tree_id(tree_id);
root_node.set_children(vec![graft_node_id]);
let root_tree = accesskit::Tree {
root: root_node_id,
toolkit_name: None,
toolkit_version: None,
};
let root_update = TreeUpdate {
nodes: vec![(root_node_id, root_node), (graft_node_id, graft_node)],
tree: Some(root_tree),
tree_id: TreeId::ROOT,
focus: root_node_id,
};
let mut tree = accesskit_consumer::Tree::new(root_update, true );
for tree_update in tree_updates {
tree.update_and_process_changes(tree_update, &mut NoOpChangeHandler);
}
tree
}
fn assert_tree_structure_and_get_root_web_area<'tree>(
tree: &'tree accesskit_consumer::Tree,
) -> accesskit_consumer::Node<'tree> {
let root_node = tree.state().root();
let scroll_view = find_first_matching_node(root_node, |node| node.role() == Role::ScrollView)
.expect("Tree should include a scroll view corresponding to the WebView.");
let scroll_view_children: Vec<accesskit_consumer::Node<'_>> = scroll_view.children().collect();
assert_eq!(scroll_view_children.len(), 1);
let graft_node = scroll_view_children[0];
assert!(graft_node.is_graft());
find_first_matching_node(graft_node, |node| node.role() == Role::RootWebArea)
.expect("Should have a RootWebArea")
}
fn find_first_matching_node(
root_node: accesskit_consumer::Node<'_>,
mut predicate: impl FnMut(&accesskit_consumer::Node) -> bool,
) -> Option<accesskit_consumer::Node<'_>> {
let mut children = root_node.children().collect::<VecDeque<_>>();
while let Some(candidate) = children.pop_front() {
if predicate(&candidate) {
return Some(candidate);
}
for child in candidate.children() {
children.push_back(child);
}
}
None
}
fn find_all_matching_nodes(
root_node: accesskit_consumer::Node<'_>,
mut predicate: impl FnMut(&accesskit_consumer::Node) -> bool,
) -> Vec<accesskit_consumer::Node<'_>> {
let mut children = root_node.children().collect::<VecDeque<_>>();
let mut result = vec![];
while let Some(candidate) = children.pop_front() {
if predicate(&candidate) {
result.push(candidate);
}
for child in candidate.children() {
children.push_back(child);
}
}
result
}
fn wait_for_webview_scene_to_be_up_to_date(servo_test: &ServoTest, webview: &WebView) {
let waiting = Rc::new(Cell::new(true));
let callback_waiting = waiting.clone();
webview.take_screenshot(None, move |result| {
assert!(result.is_ok());
callback_waiting.set(false);
});
servo_test.spin(|| waiting.get());
}