use crate::accessibility::AXTree;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation, WcagResults};
pub const RULE_META_VIEWPORT_LARGE: RuleMetadata = RuleMetadata {
id: "1.4.4",
name: "Meta Viewport Large Scale",
level: WcagLevel::AA,
severity: Severity::Medium,
description: "The viewport meta tag must allow users to scale the page to at least 500%",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html",
axe_id: "meta-viewport-large",
tags: &["wcag2aa", "wcag144", "cat.sensory-and-visual-cues"],
};
pub fn check_meta_viewport_large(tree: &AXTree) -> WcagResults {
let mut results = WcagResults::new();
for node in tree.iter() {
if node.ignored {
continue;
}
results.nodes_checked += 1;
let role = match node.role.as_deref() {
Some(r) => r.to_lowercase(),
None => continue,
};
if !matches!(role.as_str(), "rootwebarea" | "document" | "webarea") {
continue;
}
let viewport = match node.get_property_str("viewport") {
Some(v) => v,
None => continue,
};
if is_viewport_restricted(viewport) {
results.add_violation(
Violation::new(
RULE_META_VIEWPORT_LARGE.id,
RULE_META_VIEWPORT_LARGE.name,
RULE_META_VIEWPORT_LARGE.level,
RULE_META_VIEWPORT_LARGE.severity,
"Viewport meta tag restricts zoom below 500% (maximum-scale < 5 or user-scalable=no)",
&node.node_id,
)
.with_fix(
"Set maximum-scale to at least 5 or remove maximum-scale restriction",
)
.with_rule_id(RULE_META_VIEWPORT_LARGE.axe_id)
.with_help_url(RULE_META_VIEWPORT_LARGE.help_url),
);
} else {
results.passes += 1;
}
}
results
}
fn is_viewport_restricted(viewport: &str) -> bool {
let content = viewport.to_lowercase();
if content.contains("user-scalable=no") || content.contains("user-scalable=0") {
return true;
}
if let Some(pos) = content.find("maximum-scale=") {
let after = &content[pos + "maximum-scale=".len()..];
let value_str: String = after
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
if let Ok(val) = value_str.parse::<f64>() {
return val < 5.0;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::{AXNode, AXProperty, AXTree, AXValue};
fn root_with_viewport(viewport: &str) -> AXNode {
AXNode {
node_id: "root".into(),
ignored: false,
ignored_reasons: vec![],
role: Some("RootWebArea".into()),
name: Some("Test Page".into()),
name_source: None,
description: None,
value: None,
properties: vec![AXProperty {
name: "viewport".into(),
value: AXValue::String(viewport.into()),
}],
child_ids: vec![],
parent_id: None,
backend_dom_node_id: None,
}
}
#[test]
fn test_maximum_scale_below_5_flagged() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, maximum-scale=3",
)]);
let r = check_meta_viewport_large(&tree);
assert!(r
.violations
.iter()
.any(|v| v.rule_id.as_deref() == Some("meta-viewport-large")));
}
#[test]
fn test_user_scalable_no_flagged() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, user-scalable=no",
)]);
let r = check_meta_viewport_large(&tree);
assert!(r
.violations
.iter()
.any(|v| v.rule_id.as_deref() == Some("meta-viewport-large")));
}
#[test]
fn test_maximum_scale_5_passes() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, maximum-scale=5",
)]);
let r = check_meta_viewport_large(&tree);
assert!(r.violations.is_empty());
assert!(r.passes >= 1);
}
#[test]
fn test_no_maximum_scale_passes() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, initial-scale=1",
)]);
let r = check_meta_viewport_large(&tree);
assert!(r.violations.is_empty());
assert!(r.passes >= 1);
}
#[test]
fn test_maximum_scale_10_passes() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, maximum-scale=10",
)]);
let r = check_meta_viewport_large(&tree);
assert!(r.violations.is_empty());
}
#[test]
fn test_is_viewport_restricted() {
assert!(is_viewport_restricted("maximum-scale=1"));
assert!(is_viewport_restricted("maximum-scale=4.9"));
assert!(is_viewport_restricted("user-scalable=no"));
assert!(is_viewport_restricted("user-scalable=0"));
assert!(!is_viewport_restricted(
"width=device-width, initial-scale=1"
));
assert!(!is_viewport_restricted("maximum-scale=5"));
assert!(!is_viewport_restricted("maximum-scale=10"));
}
}