krz/domain-dig
an ios app for DNS & SSL analysis
clone: git clone https://gitbay.org/krz/domain-dig.git
v5.0.0: 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}