use crate::accessibility::AXTree;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation, WcagResults};
pub const RESIZE_TEXT_RULE: RuleMetadata = RuleMetadata {
id: "1.4.4",
name: "Resize Text",
level: WcagLevel::AA,
severity: Severity::High,
description: "Text can be resized up to 200% without loss of content or functionality",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html",
axe_id: "meta-viewport",
tags: &["wcag2aa", "wcag144", "cat.sensory-and-visual-cues"],
};
pub fn check_resize_text(tree: &AXTree) -> WcagResults {
let mut results = WcagResults::new();
if let Some(root) = tree.root() {
let viewport_info = root.get_property_str("viewport");
if let Some(viewport) = viewport_info {
let vp_lower = viewport.to_lowercase();
if vp_lower.contains("user-scalable=no") || vp_lower.contains("user-scalable=0") {
let violation = Violation::new(
RESIZE_TEXT_RULE.id,
RESIZE_TEXT_RULE.name,
RESIZE_TEXT_RULE.level,
Severity::High,
"Viewport meta tag prevents user scaling (user-scalable=no)",
root.node_id.clone(),
)
.with_fix("Remove user-scalable=no from viewport meta tag to allow text resizing")
.with_help_url(RESIZE_TEXT_RULE.help_url);
results.add_violation(violation);
}
if let Some(max_scale_pos) = vp_lower.find("maximum-scale=") {
let value_str = &vp_lower[max_scale_pos + 14..];
let value_end = value_str
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(value_str.len());
if let Ok(max_scale) = value_str[..value_end].parse::<f32>() {
if max_scale < 2.0 {
let violation = Violation::new(
RESIZE_TEXT_RULE.id,
RESIZE_TEXT_RULE.name,
RESIZE_TEXT_RULE.level,
Severity::Medium,
format!(
"Viewport maximum-scale={:.1} is less than 2.0, limiting text resize",
max_scale
),
root.node_id.clone(),
)
.with_fix("Set maximum-scale to at least 2.0 or remove it entirely")
.with_help_url(RESIZE_TEXT_RULE.help_url);
results.add_violation(violation);
}
}
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::accessibility::{AXNode, AXProperty, AXTree, AXValue};
fn root_with_viewport(viewport: &str) -> AXNode {
AXNode {
node_id: "root".to_string(),
ignored: false,
ignored_reasons: vec![],
role: Some("WebArea".to_string()),
name: Some("Test Page".to_string()),
name_source: None,
description: None,
value: None,
properties: vec![AXProperty {
name: "viewport".to_string(),
value: AXValue::String(viewport.to_string()),
}],
child_ids: vec![],
parent_id: None,
backend_dom_node_id: None,
}
}
#[test]
fn test_no_viewport_restriction() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, initial-scale=1",
)]);
let results = check_resize_text(&tree);
assert_eq!(results.violations.len(), 0);
}
#[test]
fn test_user_scalable_no() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, user-scalable=no",
)]);
let results = check_resize_text(&tree);
assert_eq!(results.violations.len(), 1);
assert!(results.violations[0].message.contains("user-scalable=no"));
}
#[test]
fn test_maximum_scale_too_low() {
let tree = AXTree::from_nodes(vec![root_with_viewport(
"width=device-width, maximum-scale=1.0",
)]);
let results = check_resize_text(&tree);
assert_eq!(results.violations.len(), 1);
assert!(results.violations[0].message.contains("maximum-scale"));
}
}