- 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
300 lines
12 KiB
Swift
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")
|
|
}
|
|
}
|
|
}
|