Keep manual review visible in accessibility summaries (#1224)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Earl Co 2026-10-12 08:52:35 +02:00 committed by GitHub
parent 4541974ef9
commit 51359beaea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 62 additions and 7 deletions

View File

@ -48,6 +48,10 @@ pub fn report_html(report: &Report, file_name: &str, date: &str, lang: &str, wor
// `REPORT_WORDS` can be read off this file.
h.push_str(&if report.count(Status::Failed) > 0 {
esc(&words("The checker found problems which may prevent the document from being fully accessible."))
} else if report.count(Status::Manual) > 0 {
format!("{}: {}", esc(&words(Status::Manual.label())), report.count(Status::Manual))
} else if report.count(Status::Skipped) > 0 {
format!("{}: {}", esc(&words(Status::Skipped.label())), report.count(Status::Skipped))
} else {
esc(&words("The checker found no problems in this document."))
});

View File

@ -421,6 +421,13 @@ fn rules_have_ids_and_the_report_lists_them() {
let other = report_html(&report, "a<b>.pdf", "2026-10-02", "es", &|w| format!("«{w}»"));
assert!(other.contains("<html lang=\"es\">") && other.contains("<h3>«Headings»</h3>") && other.contains("a&lt;b&gt;.pdf"));
assert!(!other.contains(">Headings<"), "an English heading is left in the report: {other}");
let manual_only = Report { results: vec![RuleResult { rule: Rule::LogicalReadingOrder, status: Status::Manual, findings: Vec::new() }] };
let manual_html = report_html(&manual_only, "review.pdf", "2026-10-02", "en", &in_english);
assert!(manual_html.contains("<p>Needs manual check: 1</p>"));
assert!(!manual_html.contains("no problems"));
let all_passed = Report { results: vec![RuleResult { rule: Rule::LogicalReadingOrder, status: Status::Passed, findings: Vec::new() }] };
let passed_html = report_html(&all_passed, "review.pdf", "2026-10-02", "en", &in_english);
assert!(passed_html.contains("<p>The checker found no problems in this document.</p>"), "{passed_html}");
}
fn figures_doc() -> Document {

View File

@ -1736,6 +1736,10 @@ fn accessibility_check_report_and_fixes_through_tools() {
assert_eq!(r["failed"].as_u64(), Some(3), "tagging, tagged content and (undone) title");
let html = std::fs::read_to_string(dir.join("report.html")).unwrap();
assert!(html.contains("Accessibility Report") && html.contains("a.pdf"));
ok(&mut a, "accessibility_report", json!({ "doc": doc, "path": "manual-report.html", "rules": ["scripts"] }));
let manual_html = std::fs::read_to_string(dir.join("manual-report.html")).unwrap();
assert!(manual_html.contains("<p>Needs manual check: 1</p>"));
assert!(!manual_html.contains("no problems"));
// Figures: list, describe, mark decorative.
std::fs::write(
dir.join("figures.pdf"),

View File

@ -176,14 +176,20 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
return action;
};
let failed = report.count(Status::Failed);
let manual = report.count(Status::Manual);
let skipped = report.count(Status::Skipped);
ui.horizontal(|ui| {
ui.label(
egui::RichText::new(if failed == 0 {
egui::RichText::new(if failed == 0 && manual == 0 && skipped == 0 {
tl!("No issues found").to_string()
} else if failed == 1 {
tl!("1 issue").to_string()
} else {
} else if failed > 1 {
crate::i18n::fmt(tl!("{n} issues"), &[("n", &failed.to_string())])
} else if manual > 0 {
format!("{}: {manual}", tl!("Needs manual check"))
} else {
format!("{}: {skipped}", tl!("Skipped"))
})
.color(t.text_muted),
);
@ -196,6 +202,9 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D
}
});
});
if failed > 0 && manual > 0 {
ui.label(egui::RichText::new(format!("{}: {manual}", tl!("Needs manual check"))).small().color(t.text_muted));
}
if let Some((_, previous)) = state.previous.as_ref().filter(|(d, _)| *d == doc) {
egui::CollapsingHeader::new(tl!("Compare")).default_open(true).show(ui, |ui| {
for status in [Status::Passed, Status::Failed, Status::Manual, Status::Skipped] {
@ -360,20 +369,26 @@ impl PdfCraftApp {
r.findings.clear();
}
let failed = report.count(Status::Failed);
let manual = report.count(Status::Manual);
let skipped = report.count(Status::Skipped);
self.a11y.previous =
if self.a11y.options.as_ref() == Some(&options) { self.a11y.report.take().filter(|(prior_id, _)| *prior_id == id) } else { None };
self.a11y.options = Some(options);
self.a11y.report = Some((id, report));
self.right = Some(RightPanel::Accessibility);
self.notify(if failed == 0 {
self.notify(if failed == 0 && manual == 0 && skipped == 0 {
tl!("The checker found no problems in this document.").to_string()
} else if failed == 1 {
tl!("The checker found 1 problem which may prevent the document from being fully accessible.").to_string()
} else {
} else if failed > 1 {
crate::i18n::fmt(
tl!("The checker found {n} problems which may prevent the document from being fully accessible."),
&[("n", &failed.to_string())],
)
} else if manual > 0 {
format!("{}: {manual}", tl!("Needs manual check"))
} else {
format!("{}: {skipped}", tl!("Skipped"))
});
if self.a11y_options.create_report {
self.show_accessibility_report();

View File

@ -92,6 +92,31 @@ fn checking_fixing_skipping_and_reporting() {
assert!(html.contains("Accessibility Report") && html.contains("Skipped"));
}
#[test]
fn checker_summary_does_not_hide_manual_review() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("notes.pdf", None, FIXTURE.to_vec()).unwrap();
app
});
h.run_steps(3);
h.state_mut().run_accessibility_check();
let report = &mut h.state_mut().a11y.report.as_mut().unwrap().1;
for result in &mut report.results {
result.status = Status::Passed;
result.findings.clear();
}
report.results[0].status = Status::Manual;
h.run_steps(2);
h.get_by_label("Needs manual check: 1");
assert!(h.query_by_label("No issues found").is_none());
// Every rule passing keeps the plain success wording.
h.state_mut().a11y.report.as_mut().unwrap().1.results[0].status = Status::Passed;
h.run_steps(2);
h.get_by_label("No issues found");
}
/// Two tagged figures: an image without alternate text and a bar (role-mapped Photo) with.
const FIGURES: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 5 0 R >> endobj

View File

@ -8390,8 +8390,8 @@ milestone = "M12"
status = "shipped"
commands = ["a11y.check"]
tools = ["accessibility_check"]
evidence = ["crates/a11y/src/tests.rs::an_untagged_document_fails_the_document_rules", "crates/a11y/src/tests.rs::a_well_tagged_document_passes", "crates/a11y/src/tests.rs::structure_findings_identify_indirect_elements_without_guessing_inline_objects", "crates/a11y/src/tests.rs::figures_are_listed_with_their_alt_text_and_place", "crates/a11y/src/tests.rs::annotation_findings_only_locate_valid_rectangles", "crates/automation/tests/automation.rs::accessibility_check_locates_field_description_on_page", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/ui-egui/tests/accessibility.rs::checker_shows_a_figure_finding_on_its_page", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools"]
notes = "full check: options dialog (report, page range, 32 rules by category, 31 on by default), results panel by category with Passed / Failed / Needs manual check / Skipped and findings that go to their page. Locatable figure, annotation and form-widget findings flash their bounds on the page; accessibility_check exposes those bounds in PDF user space. Other findings navigate to their page. Structure findings expose the indirect tag object number and generation through accessibility_check, when available, for precise follow-up in the Tags Panel."
evidence = ["crates/a11y/src/tests.rs::an_untagged_document_fails_the_document_rules", "crates/a11y/src/tests.rs::a_well_tagged_document_passes", "crates/a11y/src/tests.rs::structure_findings_identify_indirect_elements_without_guessing_inline_objects", "crates/a11y/src/tests.rs::figures_are_listed_with_their_alt_text_and_place", "crates/a11y/src/tests.rs::annotation_findings_only_locate_valid_rectangles", "crates/automation/tests/automation.rs::accessibility_check_locates_field_description_on_page", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/ui-egui/tests/accessibility.rs::checker_shows_a_figure_finding_on_its_page", "crates/ui-egui/tests/accessibility.rs::checker_summary_does_not_hide_manual_review", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools"]
notes = "full check: options dialog (report, page range, 32 rules by category, 31 on by default), results panel by category with Passed / Failed / Needs manual check / Skipped and findings that go to their page. A zero-failure check reports manual or skipped rules rather than claiming no issues. Locatable figure, annotation and form-widget findings flash their bounds on the page; accessibility_check exposes those bounds in PDF user space. Other findings navigate to their page. Structure findings expose the indirect tag object number and generation through accessibility_check, when available, for precise follow-up in the Tags Panel."
[[feature]]
id = "a11y.checker-report"
@ -8403,7 +8403,7 @@ status = "shipped"
commands = ["a11y.report"]
tools = ["accessibility_report", "accessibility_review_record", "accessibility_review_html"]
evidence = ["crates/a11y/src/tests.rs::rules_have_ids_and_the_report_lists_them", "crates/ui-egui/tests/accessibility.rs::checking_fixing_skipping_and_reporting", "crates/automation/tests/automation.rs::accessibility_check_report_and_fixes_through_tools", "crates/automation/tests/automation.rs::accessibility_review_record_compares_two_versions_without_changing_them", "crates/automation/src/a11y.rs::document_names_are_escaped_in_review_html"]
notes = "HTML checker report: summary counts and a table per category with descriptions and findings. Headless comparison of two open versions exports the same before-and-after evidence as JSON or accessible, self-contained HTML with hashes, rule changes, and remaining review items."
notes = "HTML checker report: summary counts and a table per category with descriptions and findings. The headline shows manual or skipped rules when there are no automated failures. Headless comparison of two open versions exports the same before-and-after evidence as JSON or accessible, self-contained HTML with hashes, rule changes, and remaining review items."
[[feature]]
id = "a11y.checker-fix"