diff options
| author | Christian Cleberg <[email protected]> | 2026-07-23 19:20:52 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-23 19:34:57 -0500 |
| commit | 7315ae3da604debfacfed9715a04dc91139a31e0 (patch) | |
| tree | 13febd9af1f44de01f05c9aef661b23cd97e2770 /DomainDigUITests/AccessibilityScreenshotTests.swift | |
| parent | 655d11b59a273c14720952478ccebec20a9627bd (diff) | |
| download | domain-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.swift | 105 |
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 + } +} |
