use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ChildOrder {
dom_index: usize,
order: i32,
}
fn is_visually_reordered(children: &[ChildOrder]) -> bool {
let mut visual_order: Vec<ChildOrder> = children.to_vec();
visual_order.sort_by_key(|c| (c.order, c.dom_index));
children
.iter()
.zip(visual_order.iter())
.any(|(dom, visual)| dom.dom_index != visual.dom_index)
}
pub(super) const MEANINGFUL_SEQUENCE_RULE: RuleMetadata = RuleMetadata {
id: "1.3.2",
name: "Meaningful Sequence",
level: WcagLevel::A,
severity: Severity::Medium,
description:
"The visual reading order matches the order content is exposed to assistive technology",
help_url: "https://www.w3.org/WAI/WCAG22/Understanding/meaningful-sequence.html",
axe_id: "css-order-reading-sequence",
tags: &["wcag2a", "wcag132", "cat.structure"],
};
const MEANINGFUL_SEQUENCE_JS: &str = r#"
(function() {
var results = [];
var containers = document.querySelectorAll('*');
/*CSS_SELECTOR*/
function selectorFor(el) {
var s = el.tagName.toLowerCase();
if (el.id) s += '#' + el.id;
return s;
}
for (var c = 0; c < containers.length && __amsReal(results) < 5; c++) {
var container = containers[c];
var cs = getComputedStyle(container);
if (cs.display !== 'flex' && cs.display !== 'inline-flex') continue;
if (container.hasAttribute('aria-owns')) continue;
var rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) continue;
var children = [];
for (var i = 0; i < container.children.length; i++) {
var child = container.children[i];
var childCs = getComputedStyle(child);
if (childCs.display === 'none' || childCs.visibility === 'hidden') continue;
var order = parseInt(childCs.order, 10);
if (isNaN(order)) order = 0;
children.push({ selector: selectorFor(child), order: order });
}
if (children.length < 2) continue;
__amsPush(results, container, {
selector: __amsCssSelector(container),
children: children
}, 5);
}
return { results: results };
})()
"#;
pub async fn check_meaningful_sequence_with_page(page: &Page) -> Vec<Violation> {
let val = match crate::wcag::types::evaluate_or_fail(
page,
&MEANINGFUL_SEQUENCE_RULE,
&MEANINGFUL_SEQUENCE_JS.replace(
"/*CSS_SELECTOR*/",
crate::accessibility::js_helpers::CSS_SELECTOR_JS,
),
)
.await
{
Ok(v) => v,
Err(violations) => return violations,
};
let entries = match val.get("results").and_then(|v| v.as_array()) {
Some(a) => a.clone(),
None => {
return vec![crate::wcag::technical_rule_failure(
&MEANINGFUL_SEQUENCE_RULE,
"invalid_evaluation_shape",
)]
}
};
entries
.iter()
.filter_map(|entry| {
let selector = entry.get("selector").and_then(|v| v.as_str())?;
let raw_children = entry.get("children").and_then(|v| v.as_array())?;
let mut child_selectors: Vec<&str> = Vec::with_capacity(raw_children.len());
let mut child_orders: Vec<ChildOrder> = Vec::with_capacity(raw_children.len());
for c in raw_children {
let Some(selector) = c.get("selector").and_then(|v| v.as_str()) else {
continue;
};
let Some(order) = c.get("order").and_then(|v| v.as_i64()) else {
continue;
};
child_orders.push(ChildOrder {
dom_index: child_selectors.len(),
order: order as i32,
});
child_selectors.push(selector);
}
if !is_visually_reordered(&child_orders) {
return None;
}
let mut visual_order = child_orders.clone();
visual_order.sort_by_key(|c| (c.order, c.dom_index));
let dom_sequence = child_selectors.join(", ");
let visual_sequence = visual_order
.iter()
.map(|c| child_selectors[c.dom_index])
.collect::<Vec<_>>()
.join(", ");
Some(
Violation::new(
MEANINGFUL_SEQUENCE_RULE.id,
MEANINGFUL_SEQUENCE_RULE.name,
MEANINGFUL_SEQUENCE_RULE.level,
MEANINGFUL_SEQUENCE_RULE.severity,
format!(
"Flex container '{}' uses CSS `order` to visually reorder its children \
(DOM order: {}; visual order: {}) without an `aria-owns` attribute to \
keep the accessibility tree's reading order in sync.",
selector, dom_sequence, visual_sequence
),
selector,
)
.with_selector(selector)
.with_fix(
"If the order carries meaning, reorder the elements in the DOM/source instead \
of using CSS `order`, or add `aria-owns` on the container listing the children \
in their visual order so assistive technology announces the same sequence.",
)
.with_rule_id(MEANINGFUL_SEQUENCE_RULE.axe_id)
.with_help_url(MEANINGFUL_SEQUENCE_RULE.help_url)
.as_warning(),
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_metadata_is_level_a() {
assert_eq!(MEANINGFUL_SEQUENCE_RULE.id, "1.3.2");
assert_eq!(MEANINGFUL_SEQUENCE_RULE.level, WcagLevel::A);
}
fn child(dom_index: usize, order: i32) -> ChildOrder {
ChildOrder { dom_index, order }
}
#[test]
fn fires_when_css_order_swaps_sibling_sequence() {
let children = vec![child(0, 0), child(1, -1), child(2, 0)];
assert!(is_visually_reordered(&children));
}
#[test]
fn does_not_fire_for_uniform_or_monotonically_increasing_order() {
let all_default = vec![child(0, 0), child(1, 0), child(2, 0)];
assert!(!is_visually_reordered(&all_default));
let monotonic = vec![child(0, 1), child(1, 2), child(2, 3)];
assert!(!is_visually_reordered(&monotonic));
}
}