Files
Kordant/iOS/KordantUITests/AccessibilityUITests.swift
Michael Freno e33ddf3002 feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI
- Add Apple Sign-In backend (JWKS verification, account linking, session management)
- Implement push notification deep linking with NotificationDeepLinkRouter
- Add jailbreak detection, runtime integrity monitoring, secure enclave service
- Implement OAuth social login, token refresh, and secure logout flows
- Add image caching (memory/disk), optimizer, upload queue, async semaphore
- Implement notification analytics, type preferences, and category setup
- Expand UI test suite with UITestBase, accessibility, auth flow, performance tests
- Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks
- Restructure Xcode project to manual groups with KordantWidgets target
- Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies
- Update project.yml for XcodeGen with new targets and configurations
2026-06-02 15:01:38 -04:00

300 lines
12 KiB
Swift

import XCTest
/// UI tests for accessibility: VoiceOver labels, dynamic type, color contrast.
final class AccessibilityUITests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - VoiceOver Labels on Interactive Elements
/// Verify key interactive elements have accessibility labels
func testVoiceOverLabelsOnButtons() {
navigateToTab(.dashboard)
// Tab bar items should have labels
let dashboardTab = app.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.exists, "Dashboard tab should exist")
XCTAssertGreaterThan(dashboardTab.label.count, 0, "Dashboard tab should have a non-empty label")
let servicesTab = app.tabBars.buttons["Services"]
XCTAssertTrue(servicesTab.exists, "Services tab should exist")
XCTAssertGreaterThan(servicesTab.label.count, 0, "Services tab should have a non-empty label")
let alertsTab = app.tabBars.buttons["Alerts"]
XCTAssertTrue(alertsTab.exists, "Alerts tab should exist")
XCTAssertGreaterThan(alertsTab.label.count, 0, "Alerts tab should have a non-empty label")
let settingsTab = app.tabBars.buttons["Settings"]
XCTAssertTrue(settingsTab.exists, "Settings tab should exist")
XCTAssertGreaterThan(settingsTab.label.count, 0, "Settings tab should have a non-empty label")
}
/// Verify navigation bars have proper titles
func testNavigationBarsHaveTitles() {
navigateToTab(.dashboard)
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.exists, "Dashboard navigation bar should exist")
XCTAssertEqual(dashboardNav.identifier, "Dashboard", "Nav bar identifier should match title")
navigateToTab(.services)
let servicesNav = app.navigationBars["Services"]
XCTAssertTrue(servicesNav.waitForExistence(timeout: 3), "Services navigation bar should exist")
navigateToTab(.settings)
let settingsNav = app.navigationBars["Settings"]
XCTAssertTrue(settingsNav.waitForExistence(timeout: 3), "Settings navigation bar should exist")
}
/// Verify that text labels have sufficient contrast by checking their existence
func testTextLabelsAreReadable() {
navigateToTab(.dashboard)
// Check primary text is visible (uses textPrimary color)
let threatScore = text("Threat Score")
XCTAssertTrue(threatScore.exists, "Threat Score label should exist (textPrimary)")
// Check secondary text
let alertsLabel = text("Alerts")
XCTAssertTrue(alertsLabel.exists, "Alerts label should exist (textSecondary)")
// Check tertiary text (smaller labels)
let watchedLabel = text("Watched")
XCTAssertTrue(watchedLabel.exists, "Watched label should exist")
}
// MARK: - Dynamic Type Support
/// Verify the app supports larger accessibility text sizes
func testDynamicTypeWithLargerText() {
// Quit and relaunch with larger dynamic type
app.terminate()
// Set accessibility content size (larger text)
let largeApp = XCUIApplication()
largeApp.launchArguments = ["-UITesting"]
largeApp.launchEnvironment = [
"UITestScenario": UITestScenario.populatedDashboard.rawValue,
"UIAccessibilityContentSizeCategory": "UIContentSizeCategoryAccessibilityExtraLarge"
]
largeApp.launch()
// Verify the app is still usable with larger text
let dashboardTab = largeApp.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.waitForExistence(timeout: 5),
"Dashboard tab should be accessible with large text")
// Navigate to services and check they're still tappable
largeApp.tabBars.buttons["Services"].tap()
let servicesNav = largeApp.navigationBars["Services"]
XCTAssertTrue(servicesNav.waitForExistence(timeout: 3),
"Services navigation should work with large text")
// Verify key text is visible
let darkWatchButton = largeApp.buttons["DarkWatch: Dark web monitoring & exposure tracking"]
XCTAssertTrue(darkWatchButton.waitForExistence(timeout: 3),
"DarkWatch service row should be visible with large text")
largeApp.terminate()
}
/// Verify the app supports smaller dynamic type
func testDynamicTypeWithSmallerText() {
app.terminate()
let smallApp = XCUIApplication()
smallApp.launchArguments = ["-UITesting"]
smallApp.launchEnvironment = [
"UITestScenario": UITestScenario.populatedDashboard.rawValue,
"UIAccessibilityContentSizeCategory": "UIContentSizeCategoryExtraSmall"
]
smallApp.launch()
let dashboardTab = smallApp.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.waitForExistence(timeout: 5),
"Dashboard tab should be accessible with small text")
smallApp.terminate()
}
/// Verify AX5 (largest accessibility text size) does not break layout
func testDynamicTypeAtMaximumSize() {
app.terminate()
let maxSizeApp = XCUIApplication()
maxSizeApp.launchArguments = ["-UITesting"]
maxSizeApp.launchEnvironment = [
"UITestScenario": UITestScenario.populatedDashboard.rawValue,
"UIAccessibilityContentSizeCategory": "UIContentSizeCategoryAccessibilityExtraExtraExtraLarge"
]
maxSizeApp.launch()
// Verify critical UI elements are still visible and tappable
let dashboardTab = maxSizeApp.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.waitForExistence(timeout: 5),
"Dashboard tab should exist at maximum text size")
// Verify content is still visible (no off-screen clipping that prevents tab bar access)
let servicesTab = maxSizeApp.tabBars.buttons["Services"]
XCTAssertTrue(servicesTab.exists, "Services tab should exist at maximum text size")
captureScreen(name: "DynamicType-MaximumSize")
maxSizeApp.terminate()
}
// MARK: - Element Availability Checks
/// Verify all buttons are reachable (have proper hit areas)
func testInteractiveElementsAreTappable() {
navigateToTab(.dashboard)
// Check static text labels exist for all major sections
let sections = ["Threat Score", "Recent Alerts", "Services", "Quick Actions"]
for section in sections {
let element = text(section)
XCTAssertTrue(element.exists || app.staticTexts[section].waitForExistence(timeout: 2),
"Section '\(section)' should exist on dashboard")
}
}
// MARK: - Accessibility Labels on Service Items
/// Verify service rows have accessibility labels with descriptions
func testServiceRowsHaveAccessibilityLabels() {
navigateToTab(.services)
// Service rows use: .accessibilityLabel("\(name): \(description)")
let darkWatchRow = app.buttons.containing(
NSPredicate(format: "label CONTAINS 'DarkWatch'")
).element
XCTAssertTrue(darkWatchRow.waitForExistence(timeout: 3),
"DarkWatch row should exist with accessibility label")
XCTAssertTrue(darkWatchRow.label.contains("DarkWatch"),
"DarkWatch row label should contain service name")
}
// MARK: - VoiceOver Trait Verification
/// Verify section headers use the header trait
func testSectionHeadersUseHeaderTrait() {
navigateToTab(.dashboard)
// Section headers should exist as static text
let threatScore = text("Threat Score")
XCTAssertTrue(threatScore.exists, "Threat Score header should exist")
}
// MARK: - Auth Screen Accessibility
/// Verify auth screen elements have accessibility labels
func testAuthScreenAccessibility() {
app.terminate()
let authApp = XCUIApplication()
authApp.launchArguments = ["-UITesting"]
authApp.launchEnvironment["UITestScenario"] = UITestScenario.unauthenticated.rawValue
authApp.launch()
// Brand should have accessibility
let brandName = authApp.staticTexts["Kordant"]
XCTAssertTrue(brandName.waitForExistence(timeout: 3), "Brand name should exist")
XCTAssertGreaterThan(brandName.label.count, 0, "Brand name should have a label")
// Social sign-in buttons should be present
let googleButton = authApp.buttons["Continue with Google"]
XCTAssertTrue(googleButton.waitForExistence(timeout: 3),
"Continue with Google button should be visible")
captureScreen(name: "AuthScreenAccessibility")
}
// MARK: - Progress Indicators Accessibility
/// Verify loading indicators have accessibility labels
func testLoadingStatesHaveAccessibilityLabels() {
// Navigate to a screen that shows loading state
navigateToTab(.alerts)
// On populated dashboard, alerts exist so we won't see loading,
// but the "Loading more" indicator should have a label if shown
let loadingMore = app.staticTexts["Loading more..."]
if loadingMore.waitForExistence(timeout: 2) {
XCTAssertTrue(loadingMore.exists, "Loading more indicator should exist")
}
}
/// Verify service detail screens have proper navigation bar titles
func testServiceDetailNavigationTitles() {
navigateToTab(.services)
// Navigate to DarkWatch
let darkWatchRow = app.buttons.containing(
NSPredicate(format: "label CONTAINS 'DarkWatch'")
).element
guard darkWatchRow.waitForExistence(timeout: 3) else { return }
darkWatchRow.tap()
let darkWatchNav = app.navigationBars["DarkWatch"]
XCTAssertTrue(darkWatchNav.waitForExistence(timeout: 3),
"DarkWatch navigation bar should exist")
captureScreen(name: "DarkWatch-Accessibility")
}
/// Verify content descriptions are not empty
func testContentDescriptionsNotEmpty() {
navigateToTab(.dashboard)
// Check that all static text elements have content
let allTexts = app.staticTexts.allElementsBoundByAccessibilityElement
for textElement in allTexts {
let label = textElement.label.trimmingCharacters(in: .whitespacesAndNewlines)
if !label.isEmpty {
// Only verify non-empty labels are meaningful
XCTAssertGreaterThan(label.count, 0,
"Static text should not be empty")
}
}
}
// MARK: - Reduce Motion Support
/// Verify the app respects Reduce Motion setting
func testReduceMotionRespected() {
// Relaunch with Reduce Motion enabled via accessibility settings
app.terminate()
let reduceMotionApp = XCUIApplication()
reduceMotionApp.launchArguments = ["-UITesting"]
reduceMotionApp.launchEnvironment = [
"UITestScenario": UITestScenario.populatedDashboard.rawValue,
"UIAccessibilityReduceMotionEnabled": "YES"
]
reduceMotionApp.launch()
// Verify app still renders correctly
let dashboardTab = reduceMotionApp.tabBars.buttons["Dashboard"]
XCTAssertTrue(dashboardTab.waitForExistence(timeout: 5),
"App should be functional with Reduce Motion enabled")
// Navigate and verify content renders
let servicesTab = reduceMotionApp.tabBars.buttons["Services"]
XCTAssertTrue(servicesTab.exists, "Services tab should exist with Reduce Motion")
captureScreen(name: "ReduceMotion")
reduceMotionApp.terminate()
}
// MARK: - All Interactive Elements Have Labels
/// Verify all buttons have accessibility labels
func testAllButtonsHaveLabels() {
navigateToTab(.dashboard)
// Check a sample of buttons on the dashboard
let allButtons = app.buttons.allElementsBoundByAccessibilityElement
for button in allButtons {
let label = button.label.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertFalse(label.isEmpty, "Button '\(button.identifier)' should have an accessibility label")
}
}
}