Commit 80cb5d8553
Unsigned
Layout: unified · split
Docs/ACCESSIBILITY_VERIFICATION.md added +346
| @@ -0,0 +1,346 @@ | ||
| 1 | # Accessibility Verification Checklist (issue #21, Phase 6) | |
| 2 | ||
| 3 | Manual verification for the accessibility work in phases 1–5. Everything here is | |
| 4 | what the automated audit (`Scripts/audit-a11y.sh`) **cannot** check: VoiceOver | |
| 5 | speech, the rotor, custom-content ordering, announcements, Voice Control, | |
| 6 | motion/transparency/colour settings, keyboard focus order, and the two system | |
| 7 | design languages. A green audit is necessary, not sufficient — this is what makes | |
| 8 | it sufficient. | |
| 9 | ||
| 10 | Each item traces to the phase that introduced it (e.g. `[P4]`) so a failure points | |
| 11 | straight at the code. Check the box only when the **Expected** line is literally | |
| 12 | true on the device. | |
| 13 | ||
| 14 | --- | |
| 15 | ||
| 16 | ## 0. Setup | |
| 17 | ||
| 18 | ### Devices / runtimes | |
| 19 | ||
| 20 | The app supports **iOS 17.6+** and renders under two system design languages: | |
| 21 | classic chrome (17.6–25) and Liquid Glass (26+). Materials, surfaces, and | |
| 22 | contrast resolve differently between them, so visual passes need both. | |
| 23 | ||
| 24 | - [ ] Primary device on **iOS 26+** (Liquid Glass). | |
| 25 | - [ ] A device or simulator on the **floor** (oldest available ≥ 17.6; 18.x is | |
| 26 | the practical minimum since no 17.6 runtime ships). A simulator is fine for | |
| 27 | the visual and Dynamic Type passes; VoiceOver/Voice Control are best on | |
| 28 | hardware. | |
| 29 | ||
| 30 | ### Seed data — required, or half the checklist is untestable | |
| 31 | ||
| 32 | Several items only render with tracked domains and a completed lookup. The audit | |
| 33 | simulator has none, which is exactly why the dense rows and widget are unverified | |
| 34 | so far. Before starting: | |
| 35 | ||
| 36 | - [ ] Inspect tab → run a lookup on a **live** domain (e.g. `cleberg.net`), let | |
| 37 | all sections load. | |
| 38 | - [ ] Run a lookup on a domain with a **weak/expiring or missing** cert and | |
| 39 | missing SPF/DMARC, so warning/critical tones and badges actually appear. | |
| 40 | - [ ] Track **at least 4** domains with mixed health (one healthy, one warning, | |
| 41 | one critical, one unreachable) so Dashboard tiles, dense watchlist rows, and | |
| 42 | the widget all have content. | |
| 43 | - [ ] Run a **Bulk** lookup on ~5 domains so `BatchResultRowView` renders. | |
| 44 | - [ ] Add the **Domain Portfolio** widget to the Home Screen in all three sizes | |
| 45 | (small, medium, large). | |
| 46 | - [ ] Force the Pro tier if needed so gated screens (Workflows, Scheduled | |
| 47 | Reports, Compare) are reachable. | |
| 48 | ||
| 49 | ### Settings map (paths used throughout) | |
| 50 | ||
| 51 | | Setting | Path | | |
| 52 | | --- | --- | | |
| 53 | | VoiceOver | Settings → Accessibility → VoiceOver | | |
| 54 | | Screen Curtain | VoiceOver on → triple-tap with 3 fingers | | |
| 55 | | Voice Control | Settings → Accessibility → Voice Control | | |
| 56 | | Larger Text / Dynamic Type | Settings → Accessibility → Display & Text Size → Larger Text | | |
| 57 | | Bold Text | Settings → Accessibility → Display & Text Size → Bold Text | | |
| 58 | | Increase Contrast | Settings → Accessibility → Display & Text Size → Increase Contrast | | |
| 59 | | Differentiate Without Color | Settings → Accessibility → Display & Text Size → Differentiate Without Color | | |
| 60 | | Smart Invert | Settings → Accessibility → Display & Text Size → Smart Invert | | |
| 61 | | Reduce Motion | Settings → Accessibility → Motion → Reduce Motion | | |
| 62 | | Reduce Transparency | Settings → Accessibility → Display & Text Size → Reduce Transparency | | |
| 63 | | Full Keyboard Access (iPad) | Settings → Accessibility → Keyboards → Full Keyboard Access | | |
| 64 | | App appearance override | In-app: Settings tab → Display → Appearance | | |
| 65 | ||
| 66 | > Ordering note: the passes are grouped so each iOS setting is toggled **once**. | |
| 67 | > Do them top to bottom to avoid thrashing Settings. | |
| 68 | ||
| 69 | --- | |
| 70 | ||
| 71 | ## 1. Baseline visual — Light, Dark, System `[P1][P2]` | |
| 72 | ||
| 73 | No assistive tech on. Toggle appearance via **in-app Settings → Display → | |
| 74 | Appearance**, then confirm the system setting is also honoured. | |
| 75 | ||
| 76 | - [ ] **System** appearance follows the device; flipping the device Light/Dark | |
| 77 | flips the app. | |
| 78 | - [ ] **Light** and **Dark** overrides hold regardless of the device setting. | |
| 79 | - [ ] Accent is **blue** everywhere — tab bar selection, links, section titles' | |
| 80 | "info" accents, the Insights icons, the selected quick-filter chip. No | |
| 81 | leftover **cyan**. `[P1]` | |
| 82 | - [ ] Warning tone reads as **orange**, not olive/brown — check a "Warning" or | |
| 83 | "Expiring" badge and the Dashboard "Warning" tile. `[P1 rebalance]` | |
| 84 | - [ ] The selected Dashboard summary tile is a soft **blue-tinted** surface, not | |
| 85 | lavender/violet. `[P2 wash fix]` | |
| 86 | - [ ] Prominent buttons (**Run**, **Run Batch**, **Scan**) show a **white label | |
| 87 | on a blue fill** that is comfortably readable in both schemes. `[P1 AccentFill]` | |
| 88 | - [ ] Secondary/detail text (row labels, timestamps, "Monitoring off") is legible | |
| 89 | in **Light** — not washed-out grey. `[P2 AppTextSecondary]` | |
| 90 | - [ ] Status badges pair an **icon + text + colour** (e.g. lock + "Valid"), | |
| 91 | never colour alone. | |
| 92 | - [ ] Repeat the whole list on the **floor runtime**. Note any Liquid-Glass-only | |
| 93 | difference. `[cross-runtime]` | |
| 94 | ||
| 95 | --- | |
| 96 | ||
| 97 | ## 2. Dynamic Type & reflow — up to Accessibility 5 `[P3]` | |
| 98 | ||
| 99 | Larger Text → drag to **maximum** (AX5). Walk every primary screen: Inspect | |
| 100 | (with results), Dashboard, Audit, History, Settings, Watchlist, a tracked-domain | |
| 101 | detail, Workflows. | |
| 102 | ||
| 103 | - [ ] All body text **scales up** (it already did pre-P3; confirm nothing is | |
| 104 | pinned). The Dashboard tile numbers scale too. `[P3 fixed .system(size:)]` | |
| 105 | - [ ] **No clipped headings.** Empty-state titles ("No Portfolio Yet", "No Audits | |
| 106 | Yet", "No History Yet", "No Tracked Domains", "No Batch Results Yet") **wrap | |
| 107 | onto multiple lines** rather than truncating with "…". `[P3 Label→HStack]` | |
| 108 | - [ ] **No card requires horizontal scrolling.** Inspect result cards, the risk | |
| 109 | card, and detail rows **wrap vertically**; there is no hidden left-right | |
| 110 | gesture to reach content. `[P3 CardView reflow]` | |
| 111 | - [ ] Dense rows (`BatchResultRowView`, `WatchlistRowView`) remain **readable** — | |
| 112 | text may be tall but must not overlap the trailing badge or clip. If it | |
| 113 | does, that is the deferred `ViewThatFits` work, not a P3 regression — log it. | |
| 114 | `[deferred]` | |
| 115 | - [ ] Every tappable control is at least **44×44pt** at default size. Spot-check | |
| 116 | the **copy buttons** on data rows (the most-repeated control), the | |
| 117 | **collapsible section headers**, and **Run**. `[P3 tap targets]` | |
| 118 | - [ ] Turn on **Bold Text**; confirm no layout breaks and contrast holds. | |
| 119 | - [ ] **Widget**: at AX sizes the widget content stays readable and is **not | |
| 120 | truncated into nonsense** — it clamps at Accessibility 1 by design. `[P3 widget clamp]` | |
| 121 | - [ ] Repeat the clipping/reflow spot-checks on the **floor runtime**. | |
| 122 | ||
| 123 | --- | |
| 124 | ||
| 125 | ## 3. VoiceOver `[P4]` | |
| 126 | ||
| 127 | Enable VoiceOver. Learn the gestures if needed: swipe right = next element, swipe | |
| 128 | up/down on the **rotor** set to "More Content" = reveal extra fields, two-finger | |
| 129 | swipe up = read from top. | |
| 130 | ||
| 131 | ### 3a. Icon-only controls announce a purpose, not a symbol name | |
| 132 | ||
| 133 | Focus each and confirm the spoken label. **None** should say a raw symbol name | |
| 134 | ("arrow clockwise", "square and arrow up", "bolt circle"). | |
| 135 | ||
| 136 | - [ ] Dashboard toolbar refresh → **"Refresh all tracked domains"**. | |
| 137 | - [ ] Inspect toolbar (after a lookup): clear → **"Clear results"**; actions menu | |
| 138 | → **"Actions"**; export menu → **"Export"**. | |
| 139 | - [ ] Watchlist: add → **"Add domain"**; filter → **"Filter and sort"**. | |
| 140 | - [ ] History filter → **"Filter"**. | |
| 141 | - [ ] Timeline grouping → **"Group timeline"**. | |
| 142 | - [ ] Workflows: create → **"Create workflow"**; a run summary's export → | |
| 143 | **"Export summary"**; re-run → **"Re-run workflow"**; a shared workflow's | |
| 144 | person icon → **"Shared"**. | |
| 145 | ||
| 146 | ### 3b. Toggles announce their state | |
| 147 | ||
| 148 | - [ ] Inspect **Save** (bookmark): label **"Save domain"**, value **"Not saved"**; | |
| 149 | activate → value becomes **"Saved"** and the element reports **selected**. | |
| 150 | - [ ] Domain section **Pin**: label **"Pin domain"**, value toggles | |
| 151 | **"Pinned"/"Not pinned"** and reports **selected** when pinned. | |
| 152 | - [ ] Audit checklist item: reports **selected** when complete, and a **hint** | |
| 153 | ("Marks complete" / "Marks incomplete"). | |
| 154 | - [ ] Audit area picker and Workflow domain picker rows report **selected** when | |
| 155 | chosen. | |
| 156 | ||
| 157 | ### 3c. Badges and headings | |
| 158 | ||
| 159 | - [ ] A status badge reads as **one word** — "Critical", "Valid", "Secure" — not | |
| 160 | "icon, Critical". `[P4 badge combine]` | |
| 161 | - [ ] Set the rotor to **Headings**. Section titles ("Summary", "Risk", "DNS", | |
| 162 | etc.) and the collapsible Inspect section headers are reachable as headings | |
| 163 | and let you **jump between sections**. `[P4 .isHeader]` | |
| 164 | - [ ] A collapsible Inspect header announces **"Expanded"/"Collapsed"** as its | |
| 165 | value with a hint, and toggling it updates the value. `[P4]` | |
| 166 | - [ ] Confirm the header's **trailing controls** (Track / Pin / Note on the Domain | |
| 167 | section) are still **individually focusable** — they were deliberately *not* | |
| 168 | merged into the header. `[P4 no combine on trailing]` | |
| 169 | ||
| 170 | ### 3d. Dense rows — combine + More Content rotor (the flagship) | |
| 171 | ||
| 172 | On the **Watchlist** and a **Bulk** result list, with the rotor on **More Content**: | |
| 173 | ||
| 174 | - [ ] Each row is **one VoiceOver stop**, not eight. `[P4]` | |
| 175 | - [ ] The row's **label is the domain**; its **value is the status** (e.g. | |
| 176 | "Registered" / "Critical, Registered"). | |
| 177 | - [ ] Swiping up/down on More Content reveals the extra fields **in order**: | |
| 178 | - Watchlist row: **Certificate** (spoken first, high importance), | |
| 179 | Monitoring, Updated, Pinned. | |
| 180 | - Batch row: **Risk** (high importance), IP address, Checked, Source, | |
| 181 | Impact/Status. | |
| 182 | - [ ] Risk and Certificate are spoken **without** needing the rotor (high | |
| 183 | importance); the rest wait for the swipe. | |
| 184 | ||
| 185 | ### 3e. Speech for technical strings | |
| 186 | ||
| 187 | - [ ] Focus a **DNS record value** (Inspect → DNS section) and an SSL **Cipher | |
| 188 | Suite** (Web section). Punctuation (`;`, `~`, `_`, `-`) is **spoken**, and | |
| 189 | the string reads character/token-sensibly rather than as garbled prose. | |
| 190 | `[P4 speechStyle .technical]` | |
| 191 | - [ ] Focus a plain prose value (e.g. Issuer common name) and confirm it is **not** | |
| 192 | spelled out awkwardly — only technical rows get the treatment. | |
| 193 | ||
| 194 | ### 3f. Completion announcements | |
| 195 | ||
| 196 | - [ ] Run a **single lookup**. On completion VoiceOver speaks **"Lookup complete | |
| 197 | for `<domain>`. `<availability>`."** without you moving focus. `[P4]` | |
| 198 | - [ ] Run a **sweep / Check All**. On completion it speaks **"Sweep complete. N | |
| 199 | domains, X changed, Y warnings."** `[P4]` | |
| 200 | - [ ] The announcements do **not** fire per-domain during a long sweep (would | |
| 201 | flood the queue) — only once at the end. | |
| 202 | ||
| 203 | ### 3g. Widget under VoiceOver | |
| 204 | ||
| 205 | - [ ] A medium/large widget domain row reads as one phrase, e.g. **"example.com, | |
| 206 | critical, certificate expires in 12 days"** (or "pinned", or "certificate | |
| 207 | expired"). `[P4]` | |
| 208 | - [ ] The small widget's count pills read **"N healthy"**, **"N warning"**, | |
| 209 | **"N critical"** — not a bare number. `[P4]` | |
| 210 | ||
| 211 | ### 3h. Full walkthrough with Screen Curtain | |
| 212 | ||
| 213 | Turn on Screen Curtain (triple-tap, 3 fingers — screen goes black). Complete the | |
| 214 | core journey **without looking**: | |
| 215 | ||
| 216 | - [ ] Inspect a domain → hear the sections → **Save** it → open **Watchlist** → | |
| 217 | open its **detail** → back out. Every step is discoverable and every control | |
| 218 | announces a purpose and state. Log anything that leaves you stuck. | |
| 219 | ||
| 220 | --- | |
| 221 | ||
| 222 | ## 4. Voice Control — label-in-name (WCAG 2.5.3) `[P4]` | |
| 223 | ||
| 224 | Enable Voice Control. Say the **printed** text of controls. Every visible-text | |
| 225 | control must respond to its visible name (this is why labels preserve visible | |
| 226 | text rather than replacing it). | |
| 227 | ||
| 228 | - [ ] "Tap **Run**" runs the lookup (not broken by a relabel). | |
| 229 | - [ ] "Tap **Track**", "Tap **Note**", "Tap **Compare**", "Tap **Cancel**", | |
| 230 | "Tap **Save**" each work by their printed word. | |
| 231 | - [ ] Say **"Show numbers"**; confirm the icon-only controls get numbered overlays | |
| 232 | and are operable (they have labels, so they also respond to "Show names"). | |
| 233 | - [ ] No control is reachable *only* by a name that differs from its visible text. | |
| 234 | ||
| 235 | --- | |
| 236 | ||
| 237 | ## 5. Colour & contrast settings `[P1][P2][P5]` | |
| 238 | ||
| 239 | ### 5a. Increase Contrast | |
| 240 | ||
| 241 | - [ ] Enable. Status colours and the accent shift to their **high-contrast** | |
| 242 | variants; nothing becomes unreadable in either scheme. `[P1 HC variants]` | |
| 243 | - [ ] The Settings section headers that were marginal now clear — this is the one | |
| 244 | the audit already measured (light 21 → 18). `[P2]` | |
| 245 | ||
| 246 | ### 5b. Differentiate Without Color — the P5 payoff | |
| 247 | ||
| 248 | Enable. This is the pass that validates most of Phase 5. | |
| 249 | ||
| 250 | - [ ] **Dashboard summary tiles**: the small dot becomes a **per-filter symbol** | |
| 251 | (grid, checkmark, triangle, octagon, refresh, clock, wifi-slash). `[P5]` | |
| 252 | - [ ] **Selected quick-filter chip** gains a **checkmark + border** — selection no | |
| 253 | longer depends on fill colour alone. `[P5]` | |
| 254 | - [ ] **Inspect data rows** with a warning/failure value show a **leading symbol** | |
| 255 | (triangle / octagon) before the value. `[P5 LabeledValueRow]` | |
| 256 | - [ ] Turning the setting **off** removes those extras (no permanent clutter). `[P5]` | |
| 257 | - [ ] **Widget** status already uses symbols regardless of this setting — confirm | |
| 258 | each row shows checkmark/triangle/octagon, not a bare dot. `[P5]` | |
| 259 | ||
| 260 | ### 5c. Smart Invert | |
| 261 | ||
| 262 | - [ ] Enable. UI inverts sensibly; images/icons that should stay un-inverted do. | |
| 263 | Text stays legible. Note anything that inverts wrongly. `[verification]` | |
| 264 | ||
| 265 | --- | |
| 266 | ||
| 267 | ## 6. Motion & transparency `[P5]` | |
| 268 | ||
| 269 | ### 6a. Reduce Motion | |
| 270 | ||
| 271 | Enable. Confirm each animated transition becomes an **instant** state change (no | |
| 272 | slide/fade): | |
| 273 | ||
| 274 | - [ ] Copy button check-mark swap (tap a copy button) — flips instantly. `[P5]` | |
| 275 | - [ ] Collapsible Inspect section expand/collapse — no ease animation. `[P5]` | |
| 276 | - [ ] Timeline scroll-to-section — jumps, no scroll animation. `[P5]` | |
| 277 | - [ ] Watchlist list reorder (change sort/filter) — no reflow animation. `[P5]` | |
| 278 | ||
| 279 | ### 6b. Reduce Transparency | |
| 280 | ||
| 281 | - [ ] Enable. Trigger the **Data Management** success toast (Settings → Data | |
| 282 | Management → perform a clear). Its pill background is an **opaque surface**, | |
| 283 | not a blur. `[P5]` | |
| 284 | - [ ] On **iOS 26+**, confirm system-composited chrome (nav/tab bars) still reads | |
| 285 | acceptably — the app can't declare that translucency itself. `[cross-runtime]` | |
| 286 | ||
| 287 | --- | |
| 288 | ||
| 289 | ## 7. iPad — Full Keyboard Access & split layout `[verification]` | |
| 290 | ||
| 291 | On iPad (regular width, so `RootTabView` shows the `NavigationSplitView` | |
| 292 | sidebar + detail). Enable Full Keyboard Access; attach or use the software | |
| 293 | alternative. | |
| 294 | ||
| 295 | - [ ] **Tab** moves focus in a **logical order** — sidebar → detail, top → bottom, | |
| 296 | no traps. | |
| 297 | - [ ] The blue **focus ring** is visible on every focusable control. | |
| 298 | - [ ] Sidebar tab selection and detail controls are all reachable and operable by | |
| 299 | keyboard. | |
| 300 | - [ ] Switching tabs via keyboard updates the detail pane correctly. | |
| 301 | ||
| 302 | --- | |
| 303 | ||
| 304 | ## 8. Cross-runtime sign-off `[cross-runtime]` | |
| 305 | ||
| 306 | - [ ] Sections 1, 2, 5, 6 re-checked on the **floor runtime** (classic chrome). | |
| 307 | - [ ] Sections 1, 2, 5, 6 checked on **iOS 26+** (Liquid Glass). | |
| 308 | - [ ] Any behaviour that differs between the two is logged below with the runtime | |
| 309 | noted. | |
| 310 | ||
| 311 | --- | |
| 312 | ||
| 313 | ## Sign-off | |
| 314 | ||
| 315 | | Pass | 26+ (Liquid Glass) | Floor (classic) | Notes | | |
| 316 | | --- | --- | --- | --- | | |
| 317 | | 1 Baseline visual | | | | | |
| 318 | | 2 Dynamic Type / reflow | | | | | |
| 319 | | 3 VoiceOver | n/a-runtime | | do once on hardware | | |
| 320 | | 4 Voice Control | n/a-runtime | | do once on hardware | | |
| 321 | | 5 Colour & contrast | | | | | |
| 322 | | 6 Motion & transparency | | | | | |
| 323 | | 7 iPad keyboard | n/a | | iPad only | | |
| 324 | ||
| 325 | ## Defect log | |
| 326 | ||
| 327 | Record failures here; each becomes an issue or a fix commit. | |
| 328 | ||
| 329 | | # | Pass / item | Device / runtime | Observed | Expected | Traces to | | |
| 330 | | --- | --- | --- | --- | --- | --- | | |
| 331 | | | | | | | | | |
| 332 | ||
| 333 | --- | |
| 334 | ||
| 335 | ## Known-deferred (not defects — expected gaps) | |
| 336 | ||
| 337 | - **`ViewThatFits` dense-row reflow** was not implemented (unverifiable in the | |
| 338 | audit sim). If §2 shows dense rows overlapping/truncating at AX5 with real data, | |
| 339 | that is this gap surfacing — file it against the deferred item, don't treat it | |
| 340 | as a P3 regression. | |
| 341 | - **Live Activity / Dynamic Island** custom accessibility was intentionally not | |
| 342 | added (ActivityKit exposes the labelled `ProgressView` already). Sanity-check a | |
| 343 | running sweep's Live Activity reads acceptably, but a finding here is | |
| 344 | enhancement, not regression. | |
| 345 | - **Localization** — all strings are English literals by design for now; a11y | |
| 346 | strings were written `LocalizedStringKey`-compatible for a future catalog. | |