critical priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

AccessibilityTestHarness is a Dart library (not a Flutter widget) located in test/accessibility/accessibility_test_harness.dart and importable from any test file in the project
findBySemanticsLabel(String label) returns a Finder that locates the widget whose Semantics node has a label exactly matching the given string; case-sensitive
findBySemanticsLabelContaining(String partial) returns a Finder for widgets whose Semantics label contains the given substring; case-insensitive
findBySemanticsRole(SemanticsRole role) — where SemanticsRole is a project-defined enum mapping to Flutter SemanticsFlags — returns a Finder for all nodes with the specified role (button, textField, image, checkbox, radioButton, switch, slider, link, header)
findBySemanticsHint(String hint) returns a Finder matching on Semantics hint text
expectIsButton(WidgetTester, Finder) asserts SemanticsFlag.isButton is set on the matched node
expectIsTextField(WidgetTester, Finder) asserts SemanticsFlag.isTextField is set
expectIsFocusable(WidgetTester, Finder) asserts the node has at least one focusable action (SemanticsAction.focus or isFocusable flag)
expectHasLabel(WidgetTester, Finder, String) asserts the Semantics label equals the expected string
expectHasHint(WidgetTester, Finder, String) asserts the Semantics hint equals the expected string
expectIsSelected(WidgetTester, Finder, bool) asserts SemanticsFlag.isSelected matches the expected boolean (used for tabs and checkboxes)
expectAnnounced(List<String> announcements, String expected) asserts the given string appears in the recorded announcements list
All matchers throw descriptive AssertionError messages identifying the widget path and the expected vs actual semantic property when an assertion fails
The harness includes a SemanticsAnnouncementSpy that records all SemanticsController.announce calls during a test and exposes them via a List<String> property
All public APIs have Dart doc comments with usage examples

Technical Requirements

frameworks
Flutter
flutter_test
apis
Flutter SemanticsController
Flutter SemanticsData
Flutter SemanticsFlag
Flutter SemanticsAction
Flutter Finder API (CommonFinders extension)
data models
SemanticsRole (project enum)
SemanticsAnnouncementSpy
performance requirements
Each matcher must execute in under 10ms on a pumped widget tree with up to 100 semantics nodes
SemanticsAnnouncementSpy must not introduce measurable overhead to widget pump cycles
security requirements
Harness is a test-only library — it must not be imported or referenced from production lib/ code
No file I/O or network calls — pure in-memory operations on the widget tree

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

This is a zero-dependency foundation component — it must be implemented before any other accessibility tests in the project. Implement as a set of Dart extension methods on WidgetTester (for expect* helpers) and on CommonFinders (for find* helpers) — this aligns with the existing flutter_test idiom and requires no new classes for basic usage. For the SemanticsAnnouncementSpy, subclass TestWidgetsFlutterBinding or use a ChangeNotifier listener on SemanticsController to intercept announcements without modifying production code. Define SemanticsRole as a project-level enum rather than using raw SemanticsFlag bitmasks in test code — this makes tests readable (find.bySemanticsRole(SemanticsRole.button)) and centralises the flag-to-role mapping in one place.

When a matcher fails, the error message should include: the finder description, the semantic properties found on the matched node, and the expected property — this is critical for debugging accessibility regressions quickly. Place the harness in test/accessibility/ not in lib/ — it is test infrastructure, not production code. Export all public APIs from a single test/accessibility/accessibility_test_harness.dart barrel file.

Testing Requirements

The harness itself must be thoroughly unit-tested in test/accessibility/accessibility_test_harness_test.dart. Tests: (1) findBySemanticsLabel finds a widget with the exact label and not a widget with a different label. (2) findBySemanticsLabelContaining matches partial strings case-insensitively. (3) findBySemanticsRole(button) finds an ElevatedButton and does not find a plain Text widget.

(4) findBySemanticsRole(textField) finds a TextField. (5) expectIsButton passes for a button and throws for a non-button. (6) expectIsTextField passes for a TextField and throws for a button. (7) expectHasLabel throws with a descriptive message when the label does not match, including expected and actual values.

(8) SemanticsAnnouncementSpy records an announcement made via SemanticsController.of(context).announce(). (9) expectAnnounced passes when the announcement is in the recorded list and throws when it is not. (10) All finders return findsNothing (not an error) when no matching node exists. Tag all harness tests with 'harness_meta'.

Coverage target: 95%.

Component
Accessibility Test Harness
infrastructure medium
Epic Risks (3)
high impact high prob technical

Flutter's build pipeline and SemanticsService.announce() operate asynchronously. Announcements triggered too early (before the semantic tree settles) may be swallowed silently on both platforms, causing acceptance criteria around the 500ms window to fail intermittently in CI and on device, which would block pilot launch.

Mitigation & Contingency

Mitigation: Implement the LiveRegionAnnouncer with a post-frame callback delay and an internal timing guard. Write integration tests using WidgetTester.pump() sequences that verify announcement delivery across multiple frame boundaries. Validate on physical devices at each sprint boundary, not only in CI.

Contingency: If consistent announcement timing cannot be achieved within Flutter's semantic pipeline, switch to a platform channel approach that calls native UIAccessibility.post (iOS) and AccessibilityManager.announce (Android) directly, bypassing Flutter's intermediary.

high impact medium prob technical

Flutter does not natively emit a focus-gain event to Dart code when VoiceOver or TalkBack moves focus to a specific widget. If the intercept mechanism for the SensitiveFieldWarningDialog relies on an unsupported or undocumented hook in the semantics tree, it may miss focus events for some field types or in some navigation contexts, leaving sensitive data unprotected.

Mitigation & Contingency

Mitigation: Prototype the focus-intercept mechanism during the first sprint of this epic, before building the dialog UI. Evaluate Flutter's SemanticsBinding.instance callbacks and custom SemanticsActions as intercept points. Document the chosen mechanism with platform compatibility notes.

Contingency: If no reliable focus-intercept is available, implement an alternative where sensitive fields show a static 'Screen reader active — tap to reveal' overlay instead of an OS dialog, which is less seamless but achieves equivalent privacy protection without relying on an unreliable event hook.

medium impact low prob dependency

The AccessibilityTestHarness depends on internal flutter_test semantic tree APIs that can change between Flutter minor versions. If the project upgrades Flutter during this epic, the harness may break silently, causing CI accessibility tests to pass while actually skipping assertions.

Mitigation & Contingency

Mitigation: Pin the Flutter SDK version in pubspec.yaml for the duration of this epic. Document which flutter_test APIs are used and their stability tier. Add a canary test that explicitly fails if the semantic tree API surface changes.

Contingency: If a forced Flutter upgrade breaks the harness, prioritise patching the harness as a blocking task before any other epic work continues, using the canary test failure as the trigger.