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
This commit is contained in:
299
iOS/KordantUITests/AccessibilityUITests.swift
Normal file
299
iOS/KordantUITests/AccessibilityUITests.swift
Normal file
@@ -0,0 +1,299 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user