summaryrefslogtreecommitdiff
path: root/DomainDigUITests/AccessibilityScreenshotTests.swift
blob: 9cdafac715e016cd5fa2ec91560e822e4082ccec (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
    }
}