summaryrefslogtreecommitdiff
path: root/DomainDigUITests/AccessibilityScreenshotTests.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-23 19:20:52 -0500
committerChristian Cleberg <[email protected]>2026-07-23 19:34:57 -0500
commit7315ae3da604debfacfed9715a04dc91139a31e0 (patch)
tree13febd9af1f44de01f05c9aef661b23cd97e2770 /DomainDigUITests/AccessibilityScreenshotTests.swift
parent655d11b59a273c14720952478ccebec20a9627bd (diff)
downloaddomain-dig-7315ae3da604debfacfed9715a04dc91139a31e0.tar.gz
domain-dig-7315ae3da604debfacfed9715a04dc91139a31e0.tar.bz2
domain-dig-7315ae3da604debfacfed9715a04dc91139a31e0.zip
test(a11y): Phase 6 verification — metadata assertions, middle-band sweep, 27.0 fix
Executes the Phase-6 manual runbook against the simulator, converting the mechanically-checkable parts into permanent coverage and reporting the rest honestly by tier. - Fix an enforced `.dynamicType` failure surfaced by `audit-a11y.sh current` on iOS 27.0: the Settings `Section("Services")` system header (app sets no font; 18.6 floor and 26.x CI are clean). Narrow, proven `noiseReason` carve-out scoped to dynamicType on the exact Settings header titles. Delta: current FAIL -> SUCCEEDED, finding still prints as `[noise: …]`. - AccessibilityMetadataTests: assert the icon-only control labels and the dense Watchlist/Batch row label+value contracts (green on 18.6 and 27.0). These were one-time manual VoiceOver checks; now they gate. - Middle-band Dynamic Type sweep at AccessibilityL across the seeded screens. Found no band-exclusive third bug (recorded), retained as regression insurance for a band that historically shipped two. - AccessibilityScreenshotTests: best-effort, non-gating capture utility used to produce the cross-runtime Light/Dark/AXXXL screenshots (simctl appearance does not propagate headlessly; driven through the in-app picker instead). - Docs/ACCESSIBILITY_VERIFICATION_RESULTS.md: full pass/fail/not-executable matrix + 15 screenshots. Notable positive result: Differentiate Without Color IS verifiable via the global com.apple.Accessibility defaults domain.
Diffstat (limited to 'DomainDigUITests/AccessibilityScreenshotTests.swift')
-rw-r--r--DomainDigUITests/AccessibilityScreenshotTests.swift105
1 files changed, 105 insertions, 0 deletions
diff --git a/DomainDigUITests/AccessibilityScreenshotTests.swift b/DomainDigUITests/AccessibilityScreenshotTests.swift
new file mode 100644
index 0000000..9cdafac
--- /dev/null
+++ b/DomainDigUITests/AccessibilityScreenshotTests.swift
@@ -0,0 +1,105 @@
+import XCTest
+
+/// Captures the Phase-6 visual-pass evidence as result-bundle attachments:
+/// the seeded dense screens in Light and Dark, and again at an accessibility
+/// text size. Driven from XCUITest (not `simctl`) for two reasons proven during
+/// this pass: the seed fixtures only populate under the test harness's launch,
+/// and `xcrun simctl ui appearance` does not propagate to a headless-booted
+/// simulator — so appearance is switched through the app's own Settings →
+/// Display picker, exercising the real code path.
+///
+/// **This is a capture utility, not a pass/fail test.** Every step is
+/// best-effort and never asserts: a control it cannot reach on a given runtime
+/// simply yields no screenshot, so adding it to the audit suite can never gate
+/// CI. Run with an explicit `-resultBundlePath` and export the attachments.
+@MainActor
+final class AccessibilityScreenshotTests: XCTestCase {
+ override func setUp() {
+ continueAfterFailure = true
+ }
+
+ /// Dashboard, Watchlist, and batch results in Light then Dark.
+ func testLightAndDarkScreens() {
+ let app = AccessibilityAuditHarness.launch(seeded: true)
+
+ for appearance in ["Light", "Dark"] {
+ guard setAppearance(app, to: appearance) else { continue }
+ captureSeededScreens(app, suffix: appearance.lowercased())
+ }
+ }
+
+ /// The same seeded screens at the largest accessibility text size, where
+ /// reflow and clipping surface. Launched fresh with the content-size arg.
+ func testAccessibilityTextSizeScreens() {
+ let app = AccessibilityAuditHarness.launch(
+ contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL",
+ seeded: true
+ )
+ captureSeededScreens(app, suffix: "axxxl")
+ }
+
+ // MARK: Capture (best-effort)
+
+ private func captureSeededScreens(_ app: XCUIApplication, suffix: String) {
+ app.selectRootTab("Dashboard")
+ _ = app.buttons["Refresh all tracked domains"].waitForExistence(timeout: 5)
+ attach(app, name: "dashboard-\(suffix)")
+
+ app.selectRootTab("Settings")
+ let trackedDomains = app.buttons["Tracked Domains"]
+ if trackedDomains.waitForExistence(timeout: 5) {
+ trackedDomains.tap()
+ _ = element(app, labelled: "healthy.example").waitForExistence(timeout: 5)
+ attach(app, name: "watchlist-\(suffix)")
+ }
+
+ app.selectRootTab("Inspect")
+ _ = element(app, labelled: "broken.example").waitForExistence(timeout: 5)
+ attach(app, name: "batch-\(suffix)")
+ }
+
+ private func element(_ app: XCUIApplication, labelled label: String) -> XCUIElement {
+ app.descendants(matching: .any)
+ .matching(NSPredicate(format: "label == %@", label))
+ .firstMatch
+ }
+
+ private func attach(_ app: XCUIApplication, name: String) {
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = name
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+
+ // MARK: Appearance (best-effort; returns whether it switched)
+
+ /// Drives Settings → Display → Appearance to the given option. The `Picker`
+ /// renders as an inline `.menu` whose trigger is labelled
+ /// "Appearance, <current value>". Returns `false` (rather than failing) if
+ /// any step is unreachable on this runtime.
+ private func setAppearance(_ app: XCUIApplication, to option: String) -> Bool {
+ // A prior capture may have left the Settings tab on a pushed view
+ // (Tracked Domains). Re-selecting the active tab pops it back to root.
+ app.selectRootTab("Settings")
+ let display = app.buttons["Display"]
+ if !display.waitForExistence(timeout: 2) {
+ app.selectRootTab("Settings")
+ }
+ guard display.waitForExistence(timeout: 5) else { return false }
+ display.tap()
+
+ let trigger = app.buttons
+ .matching(NSPredicate(format: "label BEGINSWITH %@", "Appearance"))
+ .firstMatch
+ guard trigger.waitForExistence(timeout: 5) else { return false }
+ trigger.tap()
+
+ // The popped menu exposes System / Light / Dark as buttons (or menu
+ // items on some runtimes).
+ let asButton = app.buttons[option]
+ let choice = asButton.waitForExistence(timeout: 3) ? asButton : app.menuItems[option]
+ guard choice.waitForExistence(timeout: 3) else { return false }
+ choice.tap()
+ return true
+ }
+}