Commit 349bec0e99

349bec0e99f330fc1172ca9847c58f019e861f61

parent: eb8a0d30da

Unsigned

cmc <hello@cleberg.net> · 2026-07-20 23:29 UTC

fix(a11y): suppress disabled-control contrast findings on the rule

WCAG 1.4.3 exempts inactive components from contrast requirements, so
Inspect's Run button — disabled until a domain is typed — was reporting a
contrast failure that was never a real defect.

The first attempt typed a domain to enable the button. That worked for
the single-screen test but raised the keyboard, which then followed the
audit onto every later screen in the Dynamic Type sweep and reported nine
phantom hit-region findings per screen against the system emoji picker's
category buttons. Suppressing on the rule instead — drop contrast
findings whose element reports isEnabled == false — fixes it everywhere
with no UI manipulation.

Also documents that simulator keyboard state persists across runs, so a
dirty simulator inflates the burndown with system-UI findings. Erase and
re-run before believing anything that names system UI.

Layout: unified · split

Docs/ACCESSIBILITY.md +13 −6
@@ -137,12 +137,19 @@ pre-commit that blocks every commit. A hook routinely bypassed with
137137
138138## Notes
139139
140- **Disabled controls are a false positive.** WCAG 1.4.3 exempts inactive
141 components from contrast requirements, but the audit flags them anyway. The
142 Inspect screen's Run button is disabled until a domain is typed, and auditing
143 the empty state reported a contrast failure that was never a real defect —
144 which is why `testInspectScreen` types a domain before auditing. Watch for
145 this before "fixing" a contrast finding on a disabled control.
140- **Disabled controls are a false positive, and are suppressed.** WCAG 1.4.3
141 exempts inactive components from contrast requirements, but the audit flags
142 them anyway — Inspect's Run button is disabled until a domain is typed, and
143 auditing the empty state reported a contrast failure that was never a real
144 defect. The harness now drops contrast findings whose element reports
145 `isEnabled == false`. Suppressing on the rule beats driving the UI to enable
146 the control: typing raises the keyboard, which then follows the audit onto
147 later screens and flags the system emoji picker's category buttons.
148- **A dirty simulator inflates the burndown.** Keyboard state persists across
149 runs, so a simulator left with the emoji picker open reports ~9 phantom
150 hit-region findings per screen. If findings appear that name system UI
151 ("Flags category", "Frequently Used category"), erase the simulator
152 (`xcrun simctl erase <udid>`) and re-run before believing them.
146153- Audits retry up to three times. Slower machines can miss the audit's internal
147154 deadline (`Audit failed to complete in time`, code `-56`), which is a tooling
148155 timeout, not an app defect. A screen that still cannot be audited is reported
DomainDigUITests/AccessibilityAuditHarness.swift +12
@@ -85,6 +85,18 @@ enum AccessibilityAuditHarness {
8585 timeout = nil
8686 do {
8787 try app.performAccessibilityAudit { issue in
88 // WCAG 1.4.3 exempts inactive components from contrast
89 // requirements, but the audit flags them anyway. Inspect's
90 // Run button is disabled until a domain is typed, so the
91 // empty state reported a contrast failure that was never a
92 // real defect. Suppressing on the rule beats driving the UI
93 // to enable the control: typing raises the keyboard, which
94 // then follows the audit onto later screens and flags the
95 // system emoji picker's category buttons.
96 if issue.auditType.contains(.contrast), issue.element?.isEnabled == false {
97 return true
98 }
99
88100 let isEnforced = !enforcedAuditTypes.intersection(issue.auditType).isEmpty
89101 let marker = isEnforced ? "FAIL" : "report"
90102 // Include the element so the burndown says *what* to fix, not
DomainDigUITests/AccessibilityAuditTests.swift +1 −14
@@ -17,20 +17,7 @@ final class AccessibilityAuditTests: XCTestCase {
1717 // MARK: Per-screen audits
1818
1919 func testInspectScreen() throws {
20 let app = AccessibilityAuditHarness.launch()
21 app.selectRootTab("Inspect")
22
23 // Type a domain so the Run button is enabled. A disabled control has no
24 // contrast requirement under WCAG 1.4.3, but the audit still flags it,
25 // so auditing the empty state would report a false positive forever.
26 let field = app.textFields.firstMatch
27 if field.waitForExistence(timeout: 5) {
28 field.tap()
29 field.typeText("example.com")
30 }
31
32 let audited = try AccessibilityAuditHarness.audit(app, screen: "inspect", test: self)
33 try XCTSkipUnless(audited, "Audit did not complete in time for Inspect")
20 try auditRootTab("Inspect")
3421 }
3522
3623 func testDashboardScreen() throws {