use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
pub const RULE_AREA_ALT: RuleMetadata = RuleMetadata {
id: "1.1.1",
name: "Area Alternative Text",
level: WcagLevel::A,
severity: Severity::High,
description: "Active <area> elements in image maps must have alternative text",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html",
axe_id: "area-alt",
tags: &["wcag2a", "wcag111", "cat.images"],
};
pub const RULE_INPUT_IMAGE_ALT: RuleMetadata = RuleMetadata {
id: "1.1.1",
name: "Image Button Alternative Text",
level: WcagLevel::A,
severity: Severity::High,
description: "<input type=\"image\"> elements must have alternative text",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html",
axe_id: "input-image-alt",
tags: &["wcag2a", "wcag111", "cat.images"],
};
pub const RULE_OBJECT_ALT: RuleMetadata = RuleMetadata {
id: "1.1.1",
name: "Object Alternative Text",
level: WcagLevel::A,
severity: Severity::High,
description: "<object> elements must have a text alternative",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html",
axe_id: "object-alt",
tags: &["wcag2a", "wcag111", "cat.text-alternatives"],
};
const IMAGE_INPUT_CAP: usize = 250;
const IMAGE_INPUT_BODY: &str = r#"
var issues = [];
var areas = document.querySelectorAll('area[href]');
for (var i = 0; i < areas.length && issues.length < CAP; i++) {
var area = areas[i];
var alt = (area.getAttribute('alt') || '').trim();
if (!alt) {
issues.push({ kind: 'area', selector: __amsCssSelector(area) });
}
}
var inputImages = document.querySelectorAll('input[type="image"]');
for (var j = 0; j < inputImages.length && issues.length < CAP; j++) {
var input = inputImages[j];
var inputAlt = (input.getAttribute('alt') || '').trim();
var ariaLabel = (input.getAttribute('aria-label') || '').trim();
if (!inputAlt && !ariaLabel) {
issues.push({ kind: 'input-image', selector: __amsCssSelector(input) });
}
}
var objects = document.querySelectorAll('object, embed');
for (var k = 0; k < objects.length && issues.length < CAP; k++) {
var obj = objects[k];
var text = (obj.textContent || '').trim();
var objAriaLabel = (obj.getAttribute('aria-label') || '').trim();
var title = (obj.getAttribute('title') || '').trim();
if (!text && !objAriaLabel && !title) {
issues.push({ kind: 'object', selector: __amsCssSelector(obj) });
}
}
return { issues: issues };
"#;
pub async fn check_image_input_rules_with_page(page: &Page) -> Vec<Violation> {
let js = [
"(function() {",
crate::accessibility::js_helpers::CSS_SELECTOR_JS,
&IMAGE_INPUT_BODY.replace("CAP", &IMAGE_INPUT_CAP.to_string()),
"})()",
]
.concat();
let val = match crate::wcag::types::evaluate_or_fail_for(
page,
"image-input-rules",
crate::cli::WcagLevel::A,
js.as_str(),
)
.await
{
Ok(v) => v,
Err(violations) => return violations,
};
let issues = match val.get("issues").and_then(|v| v.as_array()) {
Some(arr) => arr.clone(),
None => return vec![],
};
issues
.iter()
.filter_map(|issue| {
let kind = issue.get("kind")?.as_str()?;
let selector = issue.get("selector")?.as_str()?.to_string();
let (rule, message, fix) = match kind {
"area" => (
&RULE_AREA_ALT,
"Active <area> element is missing alternative text".to_string(),
"Add an alt attribute to the <area> element describing its destination",
),
"input-image" => (
&RULE_INPUT_IMAGE_ALT,
"Image submit button is missing alternative text".to_string(),
"Add an alt attribute to the <input type=\"image\"> element \
describing the button action",
),
"object" => (
&RULE_OBJECT_ALT,
"<object> element is missing a text alternative".to_string(),
"Provide a text alternative inside the <object> element or via aria-label",
),
_ => return None,
};
Some(
Violation::new(
rule.id,
rule.name,
rule.level,
rule.severity,
message,
selector.clone(),
)
.with_selector(selector)
.with_fix(fix)
.with_rule_id(rule.axe_id)
.with_help_url(rule.help_url),
)
})
.collect()
}