From 51359beaead00ef334be0f68565df6416b72290b Mon Sep 17 00:00:00 2001 From: Earl Co Date: Mon, 12 Oct 2026 08:52:35 +0200 Subject: [PATCH] Keep manual review visible in accessibility summaries (#1224) Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/a11y/src/report.rs | 4 ++++ crates/a11y/src/tests.rs | 7 +++++++ crates/automation/tests/automation.rs | 4 ++++ crates/ui-egui/src/a11y_ui.rs | 23 +++++++++++++++++++---- crates/ui-egui/tests/accessibility.rs | 25 +++++++++++++++++++++++++ parity/acrobat-features.toml | 6 +++--- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/crates/a11y/src/report.rs b/crates/a11y/src/report.rs index 97f8a4ba..a2c7b7d5 100644 --- a/crates/a11y/src/report.rs +++ b/crates/a11y/src/report.rs @@ -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.")) }); diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index fb515880..ab201b0f 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -421,6 +421,13 @@ fn rules_have_ids_and_the_report_lists_them() { let other = report_html(&report, "a.pdf", "2026-10-02", "es", &|w| format!("«{w}»")); assert!(other.contains("") && other.contains("

«Headings»

") && other.contains("a<b>.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("

Needs manual check: 1

")); + 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("

The checker found no problems in this document.

"), "{passed_html}"); } fn figures_doc() -> Document { diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index a9e4a32d..7e65d6d0 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -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("

Needs manual check: 1

")); + assert!(!manual_html.contains("no problems")); // Figures: list, describe, mark decorative. std::fs::write( dir.join("figures.pdf"), diff --git a/crates/ui-egui/src/a11y_ui.rs b/crates/ui-egui/src/a11y_ui.rs index 8e21dcbe..0fc8e2b2 100644 --- a/crates/ui-egui/src/a11y_ui.rs +++ b/crates/ui-egui/src/a11y_ui.rs @@ -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(); diff --git a/crates/ui-egui/tests/accessibility.rs b/crates/ui-egui/tests/accessibility.rs index 5fbccacf..3ac2e2db 100644 --- a/crates/ui-egui/tests/accessibility.rs +++ b/crates/ui-egui/tests/accessibility.rs @@ -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 diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 87581f68..cccdc2e1 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"