krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

main: DomainDigUITests/AccessibilityScreenshotTests.swift · raw

  1import XCTest
  2
  3/// Captures the Phase-6 visual-pass evidence as result-bundle attachments:
  4/// the seeded dense screens in Light and Dark, and again at an accessibility
  5/// text size. Driven from XCUITest (not `simctl`) for two reasons proven during
  6/// this pass: the seed fixtures only populate under the test harness's launch,
  7/// and `xcrun simctl ui appearance` does not propagate to a headless-booted
  8/// simulator  so appearance is switched through the app's own Settings 
  9/// Display picker, exercising the real code path.
 10///
 11/// **This is a capture utility, not a pass/fail test.** Every step is
 12/// best-effort and never asserts: a control it cannot reach on a given runtime
 13/// simply yields no screenshot, so adding it to the audit suite can never gate
 14/// CI. Run with an explicit `-resultBundlePath` and export the attachments.
 15@MainActor
 16final class AccessibilityScreenshotTests: XCTestCase {
 17    override func setUp() {
 18        continueAfterFailure = true
 19    }
 20
 21    /// Dashboard, Watchlist, and batch results in Light then Dark.
 22    func testLightAndDarkScreens() {
 23        let app = AccessibilityAuditHarness.launch(seeded: true)
 24
 25        for appearance in ["Light", "Dark"] {
 26            guard setAppearance(app, to: appearance) else { continue }
 27            captureSeededScreens(app, suffix: appearance.lowercased())
 28        }
 29    }
 30
 31    /// The same seeded screens at the largest accessibility text size, where
 32    /// reflow and clipping surface. Launched fresh with the content-size arg.
 33    func testAccessibilityTextSizeScreens() {
 34        let app = AccessibilityAuditHarness.launch(
 35            contentSizeCategory: "UICTContentSizeCategoryAccessibilityXXXL",
 36            seeded: true
 37        )
 38        captureSeededScreens(app, suffix: "axxxl")
 39    }
 40
 41    // MARK: Capture (best-effort)
 42
 43    private func captureSeededScreens(_ app: XCUIApplication, suffix: String) {
 44        app.selectRootTab("Dashboard")
 45        _ = app.buttons["Refresh all tracked domains"].waitForExistence(timeout: 5)
 46        attach(app, name: "dashboard-\(suffix)")
 47
 48        app.selectRootTab("Settings")
 49        let trackedDomains = app.buttons["Tracked Domains"]
 50        if trackedDomains.waitForExistence(timeout: 5) {
 51            trackedDomains.tap()
 52            _ = element(app, labelled: "healthy.example").waitForExistence(timeout: 5)
 53            attach(app, name: "watchlist-\(suffix)")
 54        }
 55
 56        app.selectRootTab("Inspect")
 57        _ = element(app, labelled: "broken.example").waitForExistence(timeout: 5)
 58        attach(app, name: "batch-\(suffix)")
 59    }
 60
 61    private func element(_ app: XCUIApplication, labelled label: String) -> XCUIElement {
 62        app.descendants(matching: .any)
 63            .matching(NSPredicate(format: "label == %@", label))
 64            .firstMatch
 65    }
 66
 67    private func attach(_ app: XCUIApplication, name: String) {
 68        let attachment = XCTAttachment(screenshot: app.screenshot())
 69        attachment.name = name
 70        attachment.lifetime = .keepAlways
 71        add(attachment)
 72    }
 73
 74    // MARK: Appearance (best-effort; returns whether it switched)
 75
 76    /// Drives Settings  Display  Appearance to the given option. The `Picker`
 77    /// renders as an inline `.menu` whose trigger is labelled
 78    /// "Appearance, <current value>". Returns `false` (rather than failing) if
 79    /// any step is unreachable on this runtime.
 80    private func setAppearance(_ app: XCUIApplication, to option: String) -> Bool {
 81        // A prior capture may have left the Settings tab on a pushed view
 82        // (Tracked Domains). Re-selecting the active tab pops it back to root.
 83        app.selectRootTab("Settings")
 84        let display = app.buttons["Display"]
 85        if !display.waitForExistence(timeout: 2) {
 86            app.selectRootTab("Settings")
 87        }
 88        guard display.waitForExistence(timeout: 5) else { return false }
 89        display.tap()
 90
 91        let trigger = app.buttons
 92            .matching(NSPredicate(format: "label BEGINSWITH %@", "Appearance"))
 93            .firstMatch
 94        guard trigger.waitForExistence(timeout: 5) else { return false }
 95        trigger.tap()
 96
 97        // The popped menu exposes System / Light / Dark as buttons (or menu
 98        // items on some runtimes).
 99        let asButton = app.buttons[option]
100        let choice = asButton.waitForExistence(timeout: 3) ? asButton : app.menuItems[option]
101        guard choice.waitForExistence(timeout: 3) else { return false }
102        choice.tap()
103        return true
104    }
105}