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") } } }