Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions SunHatUITests/AccessibilityAuditUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// AccessibilityAuditUITests.swift
// SunHatUITests
//
// Runs XCTest's built-in accessibility auditor over every primary screen.
// The auditor catches the classes of defect that are invisible in a normal
// UI test but block real users: unlabeled controls, images whose label
// repeats the trait, text that clips at large Dynamic Type sizes, contrast
// below the WCAG threshold, and hit regions under 44pt.
//
// Each screen is audited independently so a failure names the screen it came
// from. The ad SDK is disabled: Google's banner is a third-party view whose
// accessibility this project cannot fix, and its network fetch makes the
// audit nondeterministic.
//

import XCTest

final class AccessibilityAuditUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = true
}

@MainActor
private func launchApp() -> XCUIApplication {
let app = XCUIApplication()
app.launchArguments += [
"-hasCompletedOnboarding", "YES",
"-hasCreatedFirstReminder", "YES",
"-sunhatDisableAdSDK",
]
app.launch()
XCTAssertTrue(
app.tabBars.buttons["Home"].waitForExistence(timeout: 20),
"App should reach the main tab bar"
)
return app
}

/// Audits the current screen and reports every issue as a test failure
/// tagged with the screen name, so one run enumerates all of them rather
/// than stopping at the first.
@MainActor
private func audit(_ app: XCUIApplication, screen: String) throws {
try app.performAccessibilityAudit { issue in
// element carries the specific control the issue was raised
// against, which the audit type and compactDescription alone
// don't name — without it a contrast or clipping failure can't
// be traced back to the view that needs fixing.
let element = issue.element?.debugDescription ?? "no element"
XCTFail("[\(screen)] \(issue.compactDescription)\n element: \(element)")
// Already reported above; returning true stops XCTest from
// double-reporting the same issue.
return true
}
}

@MainActor
private func openTab(_ app: XCUIApplication, _ name: String) {
let tab = app.tabBars.buttons[name]
XCTAssertTrue(tab.waitForExistence(timeout: 10), "\(name) tab should exist")
tab.tap()
}

// MARK: - Per-screen audits

@MainActor
func testDashboardIsAccessible() throws {
let app = launchApp()
try audit(app, screen: "Dashboard")
}

@MainActor
func testRemindersIsAccessible() throws {
let app = launchApp()
openTab(app, "Reminders")
try audit(app, screen: "Reminders")
}

@MainActor
func testWeatherIsAccessible() throws {
let app = launchApp()
openTab(app, "Weather")
try audit(app, screen: "Weather")
}

@MainActor
func testSettingsIsAccessible() throws {
let app = launchApp()
openTab(app, "Settings")
try audit(app, screen: "Settings")
}

// MARK: - Largest accessibility text size

/// The audit's dynamicTypeSupport check only exercises the size the app is
/// currently running at, so this repeats the sweep pinned to the largest
/// accessibility size — where clipping and truncation actually appear.
@MainActor
func testPrimaryScreensAtLargestAccessibilityTextSize() throws {
let app = XCUIApplication()
app.launchArguments += [
"-hasCompletedOnboarding", "YES",
"-hasCreatedFirstReminder", "YES",
"-sunhatDisableAdSDK",
"-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL",
]
app.launch()
XCTAssertTrue(
app.tabBars.buttons["Home"].waitForExistence(timeout: 20),
"App should reach the main tab bar at AX XXXL"
)

try audit(app, screen: "Dashboard @ AX-XXXL")
for tab in ["Reminders", "Weather", "Settings"] {
openTab(app, tab)
try audit(app, screen: "\(tab) @ AX-XXXL")
}
}
}
97 changes: 97 additions & 0 deletions SunHatUITests/VisualQAScreenshotTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// VisualQAScreenshotTests.swift
// SunHatUITests
//
// Captures the four primary screens across the appearance, Dynamic Type and
// localization combinations that break Liquid Glass layouts in practice:
// dark mode (glass contrast), the largest accessibility text size (clipping
// and truncation), and Spanish (string expansion, typically 20-30% longer
// than English).
//
// These tests assert only that each screen renders and stays navigable; the
// screenshots are the deliverable, written to /tmp/sunhat-shots and attached
// to the xcresult for review.
//

import XCTest

final class VisualQAScreenshotTests: XCTestCase {
private static let screens = ["Home", "Reminders", "Weather", "Settings"]

override func setUpWithError() throws {
continueAfterFailure = false
}

@MainActor
private func launch(
extraArguments: [String] = [],
appearance: XCUIDevice.Appearance = .light
) -> XCUIApplication {
XCUIDevice.shared.appearance = appearance
let app = XCUIApplication()
app.launchArguments += [
"-hasCompletedOnboarding", "YES",
"-hasCreatedFirstReminder", "YES",
"-sunhatDisableAdSDK",
] + extraArguments
app.launch()
XCTAssertTrue(
app.tabBars.buttons["Home"].waitForExistence(timeout: 20),
"App should reach the main tab bar"
)
return app
}

@MainActor
private func captureAllScreens(_ app: XCUIApplication, variant: String) {
for screen in Self.screens {
let tab = app.tabBars.buttons[screen]
XCTAssertTrue(tab.waitForExistence(timeout: 10), "\(screen) tab should exist in \(variant)")
tab.tap()
// Let the tab's content settle before the shot; the glass tab bar
// animates its selection and the dashboard loads asynchronously.
_ = app.staticTexts.firstMatch.waitForExistence(timeout: 5)
save(app.screenshot(), name: "\(variant)-\(screen.lowercased())")
}
}

@MainActor
func testLightModeDefaultType() throws {
captureAllScreens(launch(appearance: .light), variant: "light-default")
}

@MainActor
func testDarkModeDefaultType() throws {
captureAllScreens(launch(appearance: .dark), variant: "dark-default")
}

@MainActor
func testLightModeLargestAccessibilityType() throws {
let app = launch(
extraArguments: ["-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL"],
appearance: .light
)
captureAllScreens(app, variant: "light-ax-xxxl")
}

@MainActor
func testSpanishLocalization() throws {
let app = launch(
extraArguments: ["-AppleLanguages", "(es)", "-AppleLocale", "es_ES"],
appearance: .light
)
captureAllScreens(app, variant: "spanish-default")
}

@MainActor
private func save(_ screenshot: XCUIScreenshot, name: String) {
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)

let directory = URL(fileURLWithPath: "/tmp/sunhat-shots", isDirectory: true)
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try? screenshot.pngRepresentation.write(to: directory.appendingPathComponent("\(name).png"))
}
}