use serde_json::Value;
use crate::registry::{MetricKind, CERTIFICATE, LETTER_GRADE};
use crate::taxonomy::Severity;
use super::LintFinding;
pub fn run_all_checks(report: &Value, findings: &mut Vec<LintFinding>) {
check_score_consistency(report, findings);
check_grade_certificate_consistency(report, findings);
check_severity_occurrence_sums(report, findings);
check_metric_context_matches_registry(report, findings);
check_registry_docs_urls_well_formed(findings);
check_source_quality_signals_match_findings(report, findings);
check_accessibility_breakdown_reconciles(report, findings);
}
const CHECK_BREAKDOWN_RECONCILES: &str = "accessibility_breakdown_reconciles_with_score";
fn check_accessibility_breakdown_reconciles(report: &Value, findings: &mut Vec<LintFinding>) {
let Some(summary) = report.get("summary") else {
return;
};
let Some(areas) = summary
.get("accessibility_score_breakdown")
.and_then(Value::as_array)
else {
return;
};
if areas.is_empty() {
return;
}
let Some(score) = summary.get("accessibility_score").and_then(Value::as_u64) else {
return;
};
let lost: u64 = areas
.iter()
.filter_map(|a| a.get("estimated_lost_points").and_then(Value::as_u64))
.sum();
let expected_loss = 100u64.saturating_sub(score);
if lost.abs_diff(expected_loss) > 2 {
findings.push(LintFinding {
check_id: CHECK_BREAKDOWN_RECONCILES,
evidence_path: "summary.accessibility_score_breakdown[].estimated_lost_points"
.to_string(),
expected: format!(
"lost points to sum to {expected_loss} (100 - accessibility_score {score})"
),
actual: format!("they sum to {lost}"),
severity: Severity::High,
});
}
}
const CHECK_SCORE_ALIAS: &str = "score_alias_matches_overall";
const CHECK_SINGLE_SUMMARY_MATCHES_PAGE: &str = "single_summary_matches_page";
fn check_score_consistency(report: &Value, findings: &mut Vec<LintFinding>) {
let Some(summary) = report.get("summary") else {
return;
};
if let (Some(overall), Some(alias)) = (
summary.get("overall_score").and_then(Value::as_i64),
summary.get("score").and_then(Value::as_i64),
) {
if overall != alias {
findings.push(LintFinding {
check_id: CHECK_SCORE_ALIAS,
evidence_path: "summary.score".to_string(),
expected: format!("{overall} (equal to summary.overall_score)"),
actual: alias.to_string(),
severity: Severity::High,
});
}
}
let report_type = report
.get("report_type")
.and_then(Value::as_str)
.unwrap_or("");
if report_type != "single" {
return;
}
let Some(page) = report
.get("pages")
.and_then(Value::as_array)
.and_then(|pages| pages.first())
else {
return;
};
for field in ["accessibility_score", "overall_score"] {
if let (Some(summary_val), Some(page_val)) = (
summary.get(field).and_then(Value::as_i64),
page.get(field).and_then(Value::as_i64),
) {
if summary_val != page_val {
findings.push(LintFinding {
check_id: CHECK_SINGLE_SUMMARY_MATCHES_PAGE,
evidence_path: format!("summary.{field} vs pages[0].{field}"),
expected: page_val.to_string(),
actual: summary_val.to_string(),
severity: Severity::Critical,
});
}
}
}
}
const CHECK_GRADE_MATCHES_SCORE: &str = "grade_matches_accessibility_score";
const CHECK_CERTIFICATE_MATCHES_SCORE: &str = "certificate_matches_accessibility_score";
fn risk_gate_inputs(node: &Value) -> (Option<&str>, i64, i64) {
if let Some(risk) = node.get("risk") {
let level = risk.get("level").and_then(Value::as_str);
let legal_flags = risk.get("legal_flags").and_then(Value::as_i64).unwrap_or(0);
let blocking_issues = risk
.get("blocking_issues")
.and_then(Value::as_i64)
.unwrap_or(0);
(level, legal_flags, blocking_issues)
} else {
let level = node.get("risk_level").and_then(Value::as_str);
(level, 0, 0)
}
}
fn acceptable_certificates(
score_based: &'static str,
risk_level: Option<&str>,
legal_flags: i64,
blocking_issues: i64,
) -> Vec<&'static str> {
let mut acceptable = vec![score_based];
if risk_level == Some("critical") {
acceptable.push("NICHT BESTANDEN");
}
let is_positive = matches!(score_based, "AUSBAUFÄHIG" | "STABIL" | "GUT" | "SEHR GUT");
let does_not_pass = risk_level == Some("high") || legal_flags > 0 || blocking_issues > 0;
if is_positive && does_not_pass {
acceptable.push("EINGESCHRÄNKT");
}
acceptable
}
fn check_grade_and_certificate(
node: &Value,
risk_source: &Value,
evidence_prefix: &str,
findings: &mut Vec<LintFinding>,
) {
let Some(basis) = node.get("accessibility_score").and_then(Value::as_i64) else {
return;
};
if let Some(grade) = node.get("grade").and_then(Value::as_str) {
let expected = LETTER_GRADE.label(basis as f32, false);
if grade != expected {
findings.push(LintFinding {
check_id: CHECK_GRADE_MATCHES_SCORE,
evidence_path: format!("{evidence_prefix}.grade"),
expected: expected.to_string(),
actual: grade.to_string(),
severity: Severity::Critical,
});
}
}
if let Some(certificate) = node.get("certificate").and_then(Value::as_str) {
let score_based = CERTIFICATE.label(basis as f32, false);
let (risk_level, legal_flags, blocking_issues) = risk_gate_inputs(risk_source);
let acceptable =
acceptable_certificates(score_based, risk_level, legal_flags, blocking_issues);
if !acceptable.contains(&certificate) {
findings.push(LintFinding {
check_id: CHECK_CERTIFICATE_MATCHES_SCORE,
evidence_path: format!("{evidence_prefix}.certificate"),
expected: acceptable.join(" or "),
actual: certificate.to_string(),
severity: Severity::Critical,
});
}
}
}
fn check_grade_certificate_consistency(report: &Value, findings: &mut Vec<LintFinding>) {
let report_type = report
.get("report_type")
.and_then(Value::as_str)
.unwrap_or("");
let pages = report.get("pages").and_then(Value::as_array);
if let Some(summary) = report.get("summary") {
let risk_source = if report_type == "single" {
pages.and_then(|p| p.first()).unwrap_or(summary)
} else {
summary
};
check_grade_and_certificate(summary, risk_source, "summary", findings);
}
if let Some(pages) = pages {
for (i, page) in pages.iter().enumerate() {
check_grade_and_certificate(page, page, &format!("pages[{i}]"), findings);
}
}
}
const CHECK_SEVERITY_TOTAL_SUM: &str = "severity_counts_total_matches_sum";
const CHECK_OCCURRENCE_TOTAL_SUM: &str = "occurrence_counts_total_matches_sum";
const CHECK_VIOLATED_RULE_COUNT_SCOPE: &str = "violated_rule_count_matches_severity_total";
const CHECK_VIOLATION_COUNT_SCOPE: &str = "violation_count_matches_occurrence_total";
fn counts_sum(node: &Value) -> Option<i64> {
Some(
node.get("critical")?.as_i64()?
+ node.get("high")?.as_i64()?
+ node.get("medium")?.as_i64()?
+ node.get("low")?.as_i64()?,
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ScopeRelation {
Exact,
AtMost,
}
struct CountsCheckSpec {
block_field: &'static str,
scope_count_field: &'static str,
check_total_sum: &'static str,
check_scope_match: &'static str,
}
fn check_counts_block(
node: &Value,
spec: &CountsCheckSpec,
scope_relation: ScopeRelation,
evidence_prefix: &str,
findings: &mut Vec<LintFinding>,
) {
let Some(counts) = node.get(spec.block_field) else {
return;
};
let total = counts.get("total").and_then(Value::as_i64);
if let (Some(total), Some(sum)) = (total, counts_sum(counts)) {
if total != sum {
findings.push(LintFinding {
check_id: spec.check_total_sum,
evidence_path: format!("{evidence_prefix}.{}.total", spec.block_field),
expected: sum.to_string(),
actual: total.to_string(),
severity: Severity::High,
});
}
}
if let (Some(scope_count), Some(total)) = (
node.get(spec.scope_count_field).and_then(Value::as_i64),
total,
) {
let violated = match scope_relation {
ScopeRelation::Exact => scope_count != total,
ScopeRelation::AtMost => scope_count > total,
};
if violated {
findings.push(LintFinding {
check_id: spec.check_scope_match,
evidence_path: format!("{evidence_prefix}.{}", spec.scope_count_field),
expected: match scope_relation {
ScopeRelation::Exact => total.to_string(),
ScopeRelation::AtMost => format!("<= {total}"),
},
actual: scope_count.to_string(),
severity: Severity::Medium,
});
}
}
}
fn check_severity_occurrence_sums(report: &Value, findings: &mut Vec<LintFinding>) {
let is_batch = report.get("report_type").and_then(Value::as_str) == Some("batch");
const SEVERITY_SPEC: CountsCheckSpec = CountsCheckSpec {
block_field: "severity_counts",
scope_count_field: "violated_rule_count",
check_total_sum: CHECK_SEVERITY_TOTAL_SUM,
check_scope_match: CHECK_VIOLATED_RULE_COUNT_SCOPE,
};
const OCCURRENCE_SPEC: CountsCheckSpec = CountsCheckSpec {
block_field: "occurrence_counts",
scope_count_field: "violation_count",
check_total_sum: CHECK_OCCURRENCE_TOTAL_SUM,
check_scope_match: CHECK_VIOLATION_COUNT_SCOPE,
};
let check_node = |node: &Value,
evidence_prefix: &str,
scope_relation: ScopeRelation,
findings: &mut Vec<LintFinding>| {
check_counts_block(
node,
&SEVERITY_SPEC,
scope_relation,
evidence_prefix,
findings,
);
check_counts_block(
node,
&OCCURRENCE_SPEC,
scope_relation,
evidence_prefix,
findings,
);
};
if let Some(summary) = report.get("summary") {
let summary_relation = if is_batch {
ScopeRelation::AtMost
} else {
ScopeRelation::Exact
};
check_node(summary, "summary", summary_relation, findings);
}
if let Some(pages) = report.get("pages").and_then(Value::as_array) {
for (i, page) in pages.iter().enumerate() {
check_node(page, &format!("pages[{i}]"), ScopeRelation::Exact, findings);
}
}
}
const CHECK_METRIC_CONTEXT_MATCHES_REGISTRY: &str = "metric_context_matches_registry";
type Definition = (String, String, String);
fn expected_definitions(report_type: &str, count_kind: bool) -> Vec<Definition> {
crate::registry::REGISTRY
.iter()
.filter(|m| (m.kind == MetricKind::Count) == count_kind)
.map(|m| {
(
m.json_path.to_string(),
m.unit.to_string(),
m.meaning_for(report_type).to_string(),
)
})
.collect()
}
fn actual_definitions(defs: &Value) -> Vec<Definition> {
defs.as_array()
.map(|arr| {
arr.iter()
.map(|d| {
(
field_str(d, "field"),
field_str(d, "unit"),
field_str(d, "meaning"),
)
})
.collect()
})
.unwrap_or_default()
}
fn field_str(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
fn check_metric_context_matches_registry(report: &Value, findings: &mut Vec<LintFinding>) {
let Some(metric_context) = report.get("metric_context") else {
return;
};
let report_type = report
.get("report_type")
.and_then(Value::as_str)
.unwrap_or("single");
for (json_key, count_kind) in [("score_definitions", false), ("count_definitions", true)] {
let expected = expected_definitions(report_type, count_kind);
let actual = metric_context
.get(json_key)
.map(actual_definitions)
.unwrap_or_default();
if actual != expected {
findings.push(LintFinding {
check_id: CHECK_METRIC_CONTEXT_MATCHES_REGISTRY,
evidence_path: format!("metric_context.{json_key}"),
expected: format!("{} entries matching the current registry", expected.len()),
actual: format!(
"{} entries, differs from the current registry",
actual.len()
),
severity: Severity::Medium,
});
}
}
}
const CHECK_DOCS_URL_WELL_FORMED: &str = "registry_docs_url_well_formed";
fn check_registry_docs_urls_well_formed(findings: &mut Vec<LintFinding>) {
for metric in crate::registry::REGISTRY {
let well_formed = match metric.docs_url.split_once('#') {
Some((path, anchor)) => !path.is_empty() && !anchor.is_empty(),
None => false,
};
if !well_formed {
findings.push(LintFinding {
check_id: CHECK_DOCS_URL_WELL_FORMED,
evidence_path: format!("registry[{}].docs_url", metric.id),
expected: "a non-empty '<path>#<anchor>' reference".to_string(),
actual: format!("{:?}", metric.docs_url),
severity: Severity::Low,
});
}
}
}
const CHECK_PDF_CERTIFICATE_TRACEABLE: &str = "pdf_certificate_traceable_to_json";
pub(super) fn check_pdf_certificate_traceability(
report: &Value,
typst_source: &str,
findings: &mut Vec<LintFinding>,
) {
let Some(expected) = report
.get("summary")
.and_then(|s| s.get("certificate"))
.and_then(Value::as_str)
else {
return;
};
if !typst_source.contains(expected) {
findings.push(LintFinding {
check_id: CHECK_PDF_CERTIFICATE_TRACEABLE,
evidence_path: "summary.certificate vs --typst-source".to_string(),
expected: format!("Typst source contains {expected:?}"),
actual: "not found in Typst source".to_string(),
severity: Severity::Low,
});
}
}
const CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS: &str =
"source_quality_signal_contradicts_findings";
type BacksSignal = fn(&Value) -> bool;
const SOURCE_QUALITY_WCAG_BACKED_SIGNALS: &[(&str, BacksSignal)] = &[
("ImageDescriptions", |f| criterion_is(f, "1.1.1")),
("NamedControls", |f| {
f.get("rule_id")
.and_then(Value::as_str)
.is_some_and(crate::taxonomy::is_missing_name_or_role)
}),
];
fn criterion_is(finding: &Value, criterion: &str) -> bool {
finding.get("wcag_criterion").and_then(Value::as_str) == Some(criterion)
}
fn find_source_quality_signal<'a>(source_quality: &'a Value, kind: &str) -> Option<&'a Value> {
["substance", "consistency", "authority"]
.into_iter()
.find_map(|dim| {
source_quality
.pointer(&format!("/{dim}/signals"))
.and_then(Value::as_array)
.and_then(|signals| {
signals
.iter()
.find(|s| s.get("kind").and_then(Value::as_str) == Some(kind))
})
})
}
fn wcag_finding_occurrences(page: &Value, backs_signal: BacksSignal) -> i64 {
page.get("findings")
.and_then(Value::as_array)
.map(|entries| {
entries
.iter()
.filter(|f| {
f.get("category").and_then(Value::as_str) == Some("wcag") && backs_signal(f)
})
.filter_map(|f| f.get("occurrence_count").and_then(Value::as_i64))
.sum()
})
.unwrap_or(0)
}
fn check_source_quality_signals_match_findings(
report: &Value,
findings_out: &mut Vec<LintFinding>,
) {
let Some(pages) = report.get("pages").and_then(Value::as_array) else {
return;
};
for (i, page) in pages.iter().enumerate() {
let Some(source_quality) = page.pointer("/detail/modules/source_quality") else {
continue;
};
for (kind, backs_signal) in SOURCE_QUALITY_WCAG_BACKED_SIGNALS {
let Some(signal) = find_source_quality_signal(source_quality, kind) else {
continue;
};
let present = signal
.get("present")
.and_then(Value::as_bool)
.unwrap_or(false);
if !present {
continue;
}
let occurrences = wcag_finding_occurrences(page, *backs_signal);
if occurrences > 0 {
findings_out.push(LintFinding {
check_id: CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS,
evidence_path: format!(
"pages[{i}].detail.modules.source_quality (signal {kind}) vs pages[{i}].findings"
),
expected: format!(
"signal {kind}.present=false, or 0 occurrences of its backing findings"
),
actual: format!(
"signal {kind}.present=true but {occurrences} occurrence(s) of its backing findings"
),
severity: Severity::High,
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn registry_metric_context_json(report_type: &str) -> Value {
let mut score_definitions = Vec::new();
let mut count_definitions = Vec::new();
for metric in crate::registry::REGISTRY {
let def = json!({
"field": metric.json_path,
"unit": metric.unit,
"meaning": metric.meaning_for(report_type),
});
if metric.kind == MetricKind::Count {
count_definitions.push(def);
} else {
score_definitions.push(def);
}
}
json!({
"score_scale": {"minimum": 0, "maximum": 100, "higher_is_better": true},
"score_definitions": score_definitions,
"count_definitions": count_definitions,
})
}
fn clean_single_report() -> Value {
json!({
"schema_version": "2.0",
"report_type": "single",
"metric_context": registry_metric_context_json("single"),
"summary": {
"accessibility_score": 82,
"overall_score": 82,
"score": 82,
"grade": "B",
"certificate": "GUT",
"severity_counts": {"critical": 1, "high": 2, "medium": 0, "low": 0, "total": 3},
"violated_rule_count": 3,
"occurrence_counts": {"critical": 1, "high": 3, "medium": 0, "low": 0, "total": 4},
"violation_count": 4
},
"pages": [
{
"accessibility_score": 82,
"overall_score": 82,
"grade": "B",
"certificate": "GUT",
"severity_counts": {"critical": 1, "high": 2, "medium": 0, "low": 0, "total": 3},
"violated_rule_count": 3,
"occurrence_counts": {"critical": 1, "high": 3, "medium": 0, "low": 0, "total": 4},
"violation_count": 4
}
]
})
}
#[test]
fn clean_report_yields_no_findings() {
let mut findings = Vec::new();
run_all_checks(&clean_single_report(), &mut findings);
assert!(findings.is_empty(), "unexpected findings: {findings:?}");
}
#[test]
fn detects_summary_score_alias_mismatch() {
let mut report = clean_single_report();
report["summary"]["score"] = json!(80); let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings.iter().any(|f| f.check_id == CHECK_SCORE_ALIAS));
}
#[test]
fn detects_summary_vs_page_score_mismatch() {
let mut report = clean_single_report();
report["pages"][0]["overall_score"] = json!(80);
report["pages"][0]["grade"] = json!("B");
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_SINGLE_SUMMARY_MATCHES_PAGE));
}
#[test]
fn detects_grade_from_wrong_score_base() {
let mut report = clean_single_report();
report["summary"]["grade"] = json!("A"); let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_GRADE_MATCHES_SCORE));
}
#[test]
fn detects_certificate_from_wrong_score_base() {
let mut report = clean_single_report();
report["summary"]["certificate"] = json!("SEHR GUT"); let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_CERTIFICATE_MATCHES_SCORE));
}
#[test]
fn certificate_downgraded_by_risk_gate_is_not_flagged() {
let mut report = clean_single_report();
report["summary"]["certificate"] = json!("EINGESCHRÄNKT");
report["summary"]["risk_level"] = json!("high");
report["pages"][0]["certificate"] = json!("EINGESCHRÄNKT");
report["pages"][0]["risk"] =
json!({"level": "high", "legal_flags": 0, "blocking_issues": 0});
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(
!findings
.iter()
.any(|f| f.check_id == CHECK_CERTIFICATE_MATCHES_SCORE),
"unexpected findings: {findings:?}"
);
}
#[test]
fn certificate_still_flagged_when_risk_does_not_justify_downgrade() {
let mut report = clean_single_report();
report["summary"]["certificate"] = json!("EINGESCHRÄNKT");
report["summary"]["risk_level"] = json!("low");
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_CERTIFICATE_MATCHES_SCORE));
}
#[test]
fn certificate_downgraded_by_page_blocking_issues_not_flagged_via_summary() {
let mut report = clean_single_report();
report["summary"]["certificate"] = json!("EINGESCHRÄNKT");
report["summary"]["risk_level"] = json!("medium");
report["pages"][0]["certificate"] = json!("EINGESCHRÄNKT");
report["pages"][0]["risk"] =
json!({"level": "medium", "legal_flags": 0, "blocking_issues": 4});
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(
!findings
.iter()
.any(|f| f.check_id == CHECK_CERTIFICATE_MATCHES_SCORE),
"unexpected findings: {findings:?}"
);
}
#[test]
fn detects_severity_total_sum_mismatch() {
let mut report = clean_single_report();
report["summary"]["severity_counts"]["total"] = json!(99);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_SEVERITY_TOTAL_SUM));
}
#[test]
fn detects_violated_rule_count_scope_mismatch() {
let mut report = clean_single_report();
report["summary"]["violated_rule_count"] = json!(99);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_VIOLATED_RULE_COUNT_SCOPE));
}
fn clean_batch_report() -> Value {
json!({
"schema_version": "2.0",
"report_type": "batch",
"summary": {
"accessibility_score": 82,
"overall_score": 82,
"score": 82,
"grade": "B",
"certificate": "GUT",
"severity_counts": {"critical": 0, "high": 16, "medium": 19, "low": 0, "total": 35},
"violated_rule_count": 3,
"occurrence_counts": {"critical": 0, "high": 190, "medium": 35, "low": 0, "total": 225},
"violation_count": 225
},
"pages": [
{
"accessibility_score": 82,
"overall_score": 82,
"grade": "B",
"certificate": "GUT",
"severity_counts": {"critical": 0, "high": 1, "medium": 1, "low": 0, "total": 2},
"violated_rule_count": 2,
"occurrence_counts": {"critical": 0, "high": 12, "medium": 3, "low": 0, "total": 15},
"violation_count": 15
}
]
})
}
#[test]
fn batch_summary_violated_rule_count_below_severity_total_is_not_flagged() {
let mut findings = Vec::new();
run_all_checks(&clean_batch_report(), &mut findings);
assert!(
!findings
.iter()
.any(|f| f.check_id == CHECK_VIOLATED_RULE_COUNT_SCOPE),
"unexpected findings: {findings:?}"
);
}
#[test]
fn batch_summary_violated_rule_count_above_severity_total_is_still_flagged() {
let mut report = clean_batch_report();
report["summary"]["violated_rule_count"] = json!(999);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_VIOLATED_RULE_COUNT_SCOPE));
}
#[test]
fn batch_page_violated_rule_count_mismatch_is_still_flagged_exactly() {
let mut report = clean_batch_report();
report["pages"][0]["violated_rule_count"] = json!(99);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_VIOLATED_RULE_COUNT_SCOPE));
}
#[test]
fn detects_stale_metric_context() {
let mut report = clean_single_report();
report["metric_context"]["score_definitions"] = json!([]);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_METRIC_CONTEXT_MATCHES_REGISTRY));
}
#[test]
fn missing_optional_fields_do_not_panic_or_false_positive() {
let report = json!({
"report_type": "single",
"summary": {"overall_score": 50}
});
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings.is_empty(), "unexpected findings: {findings:?}");
}
#[test]
fn pdf_certificate_traceability_passes_when_token_present() {
let report = clean_single_report(); let typst_source = "... some rendered PDF text ... GUT ... more text ...";
let mut findings = Vec::new();
check_pdf_certificate_traceability(&report, typst_source, &mut findings);
assert!(findings.is_empty(), "unexpected findings: {findings:?}");
}
fn report_with_image_signal_and_finding(present: bool, occurrence_count: i64) -> Value {
let mut report = clean_single_report();
report["pages"][0]["detail"] = json!({
"modules": {
"source_quality": {
"substance": {
"signals": [
{
"kind": "ImageDescriptions",
"present": present,
"weight": 0.15,
"detail": "...",
"values": {"count": if present { 0 } else { occurrence_count }}
}
]
}
}
}
});
report["pages"][0]["findings"] = json!([
{
"category": "wcag",
"rule_id": "a11y.alt_text.missing",
"wcag_criterion": "1.1.1",
"occurrence_count": occurrence_count
}
]);
report
}
fn named_controls_report(rule_id: &str) -> Value {
let mut report = clean_single_report();
report["pages"][0]["detail"] = json!({
"modules": {"source_quality": {"substance": {"signals": [
{"kind": "NamedControls", "present": true, "weight": 0.25,
"detail": "...", "values": {"count": 0}}
]}}}
});
report["pages"][0]["findings"] = json!([
{"category": "wcag", "rule_id": rule_id, "wcag_criterion": "4.1.2",
"occurrence_count": 4}
]);
report
}
#[test]
fn named_controls_signal_is_contradicted_only_by_missing_names() {
let flags = |rule_id: &str| {
let mut findings = Vec::new();
run_all_checks(&named_controls_report(rule_id), &mut findings);
findings
.iter()
.any(|f| f.check_id == CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS)
};
assert!(flags("a11y.interactive_name.missing"));
assert!(!flags("a11y.aria_prohibited_attr.invalid"));
assert!(!flags("a11y.aria_hidden_focus.invalid"));
}
#[test]
fn detects_source_quality_image_signal_contradicting_findings() {
let report = report_with_image_signal_and_finding(true, 42);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS));
}
#[test]
fn source_quality_image_signal_consistent_with_findings_is_not_flagged() {
let report = report_with_image_signal_and_finding(false, 42);
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(
!findings
.iter()
.any(|f| f.check_id == CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS),
"unexpected findings: {findings:?}"
);
}
#[test]
fn source_quality_image_signal_present_with_no_findings_is_not_flagged() {
let mut report = clean_single_report();
report["pages"][0]["detail"] = json!({
"modules": {
"source_quality": {
"substance": {
"signals": [
{
"kind": "ImageDescriptions",
"present": true,
"weight": 0.15,
"detail": "...",
"values": {"count": 0}
}
]
}
}
}
});
let mut findings = Vec::new();
run_all_checks(&report, &mut findings);
assert!(
!findings
.iter()
.any(|f| f.check_id == CHECK_SOURCE_QUALITY_SIGNAL_CONTRADICTS_FINDINGS),
"unexpected findings: {findings:?}"
);
}
#[test]
fn pdf_certificate_traceability_follows_the_stored_certificate_not_overall() {
let mut report = clean_single_report();
report["summary"]["accessibility_score"] = json!(25);
report["summary"]["overall_score"] = json!(43);
report["summary"]["certificate"] = json!("NICHT BESTANDEN");
let typst_source = "... NICHT BESTANDEN ...";
let mut findings = Vec::new();
check_pdf_certificate_traceability(&report, typst_source, &mut findings);
assert!(findings.is_empty(), "unexpected findings: {findings:?}");
}
#[test]
fn pdf_certificate_traceability_flags_missing_token() {
let report = clean_single_report(); let typst_source = "... some rendered PDF text without the certificate word ...";
let mut findings = Vec::new();
check_pdf_certificate_traceability(&report, typst_source, &mut findings);
assert!(findings
.iter()
.any(|f| f.check_id == CHECK_PDF_CERTIFICATE_TRACEABLE));
}
}