1#![forbid(unsafe_code)]
59
60mod extra;
61mod finding;
62mod outcome;
63mod report;
64
65pub use extra::Extra;
66pub use finding::{Evidence, Finding, Location};
67pub use outcome::{Outcome, Severity, WcagLevel};
68pub use report::{NotRun, Report, RuleRun, Summary};
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn zaehlt_zustaende_und_schweregrade_getrennt() {
76 let mut r = Report::new();
77 r.push(Finding::fail("a", "x").with_severity(Severity::Critical));
78 r.push(Finding::review("b", "x").with_severity(Severity::Low));
79 r.push(Finding::pass("c", "x").with_severity(Severity::Critical));
80 r.push(Finding::untested("d", "x").with_severity(Severity::High));
81 let r = r.finish();
82
83 assert_eq!(
84 (
85 r.summary.fail,
86 r.summary.review,
87 r.summary.pass,
88 r.summary.untested
89 ),
90 (1, 1, 1, 1)
91 );
92 assert_eq!(
95 (r.summary.critical, r.summary.high, r.summary.low),
96 (1, 0, 1)
97 );
98 assert_eq!(r.summary.problems(), 2);
99 }
100
101 #[test]
102 fn pass_wird_standardmaessig_nicht_angezeigt() {
103 let mut r = Report::new();
104 r.push(Finding::fail("a", "x"));
105 r.push(Finding::pass("b", "x"));
106 r.push(Finding::untested("c", "x"));
107 assert_eq!(r.visible().count(), 2);
108 assert_eq!(r.by_outcome(Outcome::Pass).count(), 1);
109 }
110
111 #[test]
112 fn untested_ist_kein_problem() {
113 assert!(Outcome::Fail.is_problem());
114 assert!(Outcome::Review.is_problem());
115 assert!(!Outcome::Pass.is_problem());
116 assert!(!Outcome::Untested.is_problem());
117 }
118
119 #[test]
120 fn json_bleibt_schlank_wenn_nichts_gesetzt_ist() {
121 let f = Finding::fail("images/alt-missing", "kein alt");
122 let j: serde_json::Value = serde_json::to_value(&f).unwrap();
123 let obj = j.as_object().unwrap();
124 assert_eq!(
126 obj.len(),
127 4,
128 "unerwartete Felder: {:?}",
129 obj.keys().collect::<Vec<_>>()
130 );
131 assert_eq!(obj["outcome"], "fail");
132 assert_eq!(obj["severity"], "medium");
133 }
134
135 #[test]
136 fn json_rundlauf_erhaelt_alles() {
137 let f = Finding::review("aria/role-invalid", "unbekannte Rolle")
138 .with_severity(Severity::High)
139 .with_wcag(["4.1.2"])
140 .with_wcag_level(WcagLevel::A)
141 .with_tags(["best-practice"])
142 .with_help("Nur Rollen aus der ARIA-Spezifikation verwenden.")
143 .with_help_url("https://www.w3.org/TR/wai-aria-1.2/#roles")
144 .with_suggestion("role=\"buton\" ist vermutlich role=\"button\".")
145 .with_suggested_code("<div role=\"button\">")
146 .with_snippet("<div role=\"buton\">")
147 .at(Location::node("42").with_selector("#menu > div"))
148 .add_evidence(Evidence::dom_attribute("role", Some("buton".into())));
149
150 let json = serde_json::to_string(&f).unwrap();
151 let back: Finding = serde_json::from_str(&json).unwrap();
152 assert_eq!(f, back);
153 }
154
155 #[test]
156 fn wcag_stufen_schliessen_ein() {
157 assert!(WcagLevel::A.included_in(WcagLevel::AA));
158 assert!(WcagLevel::AA.included_in(WcagLevel::AA));
159 assert!(!WcagLevel::AAA.included_in(WcagLevel::AA));
160 assert!(WcagLevel::AAA.included_in(WcagLevel::AAA));
161 }
162
163 #[test]
164 fn severity_ordnet_sich_aufsteigend() {
165 let mut v = vec![
166 Severity::High,
167 Severity::Low,
168 Severity::Critical,
169 Severity::Medium,
170 ];
171 v.sort();
172 assert_eq!(
173 v,
174 vec![
175 Severity::Low,
176 Severity::Medium,
177 Severity::High,
178 Severity::Critical
179 ]
180 );
181 }
182
183 #[test]
184 fn axe_impact_bleibt_kompatibel() {
185 assert_eq!(Severity::Low.as_axe_impact(), "minor");
186 assert_eq!(Severity::Medium.as_axe_impact(), "moderate");
187 assert_eq!(Severity::High.as_axe_impact(), "serious");
188 assert_eq!(Severity::Critical.as_axe_impact(), "critical");
189 }
190
191 #[test]
192 fn nicht_gelaufene_regeln_werden_gezaehlt() {
193 let mut r = Report::new();
194 r.record(RuleRun::ran("images/alt-missing", 3));
195 r.record(RuleRun::not_run("contrast/text", NotRun::CapabilityMissing));
196 r.record(RuleRun::not_run("forms/label", NotRun::NotApplicable));
197 let r = r.finish();
198 assert_eq!(r.summary.rules_not_run, 2);
199 assert!(r.rule_runs[0].did_run());
200 assert!(!r.rule_runs[1].did_run());
201 }
202
203 #[derive(Debug)]
206 struct Screenshot(Vec<u8>);
207
208 #[test]
209 fn beigabe_kommt_typisiert_zurueck() {
210 let f =
211 Finding::fail("images/alt-missing", "kein alt").with_extra(Screenshot(vec![1, 2, 3]));
212 assert_eq!(f.extra.get::<Screenshot>().map(|s| s.0.len()), Some(3));
213 assert!(f.extra.get::<String>().is_none());
215 }
216
217 #[test]
218 fn beigabe_landet_nicht_im_json() {
219 let f = Finding::fail("a", "x").with_extra(Screenshot(vec![0; 4096]));
220 let j = serde_json::to_string(&f).unwrap();
221 assert!(
222 !j.contains("extra"),
223 "Beigabe darf nicht serialisiert werden: {j}"
224 );
225 assert!(j.len() < 200, "{} Bytes", j.len());
227 }
228
229 #[test]
230 fn beigabe_zaehlt_nicht_zur_identitaet() {
231 let ohne = Finding::fail("a", "x");
234 let mit = Finding::fail("a", "x").with_extra(Screenshot(vec![9]));
235 assert_eq!(ohne, mit);
236 }
237
238 #[test]
239 fn beigabe_ueberlebt_das_klonen() {
240 let f = Finding::fail("a", "x").with_extra(Screenshot(vec![7, 7]));
241 let k = f.clone();
242 assert_eq!(
243 k.extra.get::<Screenshot>().map(|s| s.0.clone()),
244 Some(vec![7, 7])
245 );
246 }
247
248 #[test]
251 fn element_und_regelname_gehen_durch_das_json() {
252 let f = Finding::fail("buttons/name-missing", "kein Name")
253 .with_rule_name("Button braucht einen zugänglichen Namen")
254 .with_element(Some("button".into()), None);
255
256 let j: serde_json::Value = serde_json::to_value(&f).unwrap();
257 assert_eq!(j["role"], "button");
258 assert_eq!(j["rule_name"], "Button braucht einen zugänglichen Namen");
259 assert!(j.get("name").is_none());
261
262 let zurueck: Finding = serde_json::from_value(j).unwrap();
263 assert_eq!(zurueck, f);
264 }
265
266 #[test]
267 fn der_schlanke_fall_bleibt_schlank() {
268 let f = Finding::fail("images/alt-missing", "kein alt");
271 let j: serde_json::Value = serde_json::to_value(&f).unwrap();
272 assert_eq!(
273 j.as_object().unwrap().len(),
274 4,
275 "unerwartete Felder: {:?}",
276 j.as_object().unwrap().keys().collect::<Vec<_>>()
277 );
278 }
279
280 #[test]
283 fn derselbe_regelvermerk_kann_je_durchgang_vorkommen() {
284 let mut r = Report::new();
287 r.record(RuleRun::ran("target-size/minimum", 0).in_viewport("desktop"));
288 r.record(RuleRun::ran("target-size/minimum", 3).in_viewport("mobile"));
289 let r = r.finish();
290
291 assert_eq!(r.rule_runs.len(), 2);
292 let mobil = r
293 .rule_runs
294 .iter()
295 .find(|x| x.viewport.as_deref() == Some("mobile"))
296 .unwrap();
297 assert_eq!(mobil.findings, 3);
298 }
299
300 #[test]
301 fn ein_nicht_gelaufener_vermerk_nennt_das_ungeprueffte_kriterium() {
302 let mut r = Report::new();
303 r.record(
304 RuleRun::not_run("contrast/text", NotRun::CapabilityMissing)
305 .with_wcag(["1.4.3"])
306 .with_reason("statische Analyse liefert keine Rendering-Werte"),
307 );
308 let r = r.finish();
309 assert_eq!(r.summary.rules_not_run, 1);
310 assert_eq!(r.rule_runs[0].wcag, vec!["1.4.3"]);
311 }
312}