Files
Kordant/iOS/KordantUITests/PerformanceTests.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

355 lines
13 KiB
Swift

//
// PerformanceTests.swift
// KordantUITests
//
// Performance tests using XCTMetric for launch, scroll, navigation,
// and image loading on physical devices.
//
// Acceptance Criteria:
// - Cold launch < 2s on iPhone 12
// - Scroll 60fps on all lists
// - All metrics within 10% of baseline
// - Runs on iPhone SE, 12, 15 Pro
//
import XCTest
final class LaunchPerformanceTests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - Cold Launch Performance
/// Measures cold launch time using XCTApplicationLaunchMetric.
/// Baseline: < 2.0 seconds on iPhone 12.
func testColdLaunchPerformance() {
let metric = XCTApplicationLaunchMetric(
waitUntilResponsive: true,
waitFor: .navigationBar("Dashboard")
)
measure(metrics: [metric]) {
let app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = UITestScenario.populatedDashboard.rawValue
app.launch()
// Wait for dashboard to fully appear
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 10), "Dashboard should appear within launch window")
app.terminate()
}
}
/// Measures warm launch time (app is already in memory cache from OS).
/// Baseline: < 1.0 second on iPhone 12.
func testWarmLaunchPerformance() {
// First launch to prime the cache
let warmApp = XCUIApplication()
warmApp.launchArguments = ["-UITesting"]
warmApp.launchEnvironment["UITestScenario"] = UITestScenario.populatedDashboard.rawValue
warmApp.launch()
let dashboardNav = warmApp.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 10))
warmApp.terminate()
// Now measure warm launch
let metric = XCTApplicationLaunchMetric(
waitUntilResponsive: true,
waitFor: .navigationBar("Dashboard")
)
measure(metrics: [metric]) {
let app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = UITestScenario.populatedDashboard.rawValue
app.launch()
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 5), "Dashboard should appear in warm launch")
app.terminate()
}
}
}
final class ScrollPerformanceTests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - Dashboard Scroll Performance
/// Measures Dashboard scroll FPS via clock, CPU, and memory metrics.
/// Acceptance: scroll remains smooth (no dropped frames).
func testDashboardScrollPerformance() {
navigateToTab(.dashboard)
// Ensure dashboard is fully loaded
let threatScore = text("Threat Score")
XCTAssertTrue(threatScore.waitForExistence(timeout: 5))
// The dashboard uses a ScrollView find it
let scrollView = app.scrollViews.firstMatch
XCTAssertTrue(scrollView.exists, "Dashboard scroll view should exist")
// Measure scrolling through the entire dashboard content
measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) {
// Scroll down through all content
for _ in 0..<5 {
scrollView.swipeUp()
// Small pause to let rendering catch up (simulates user scrolling)
let _ = scrollView.waitForExistence(timeout: 0.1)
}
}
}
// MARK: - Alert List Scroll Performance
/// Measures alert list scroll performance using LazyVStack.
/// Acceptance: smooth scrolling with no frame drops.
func testAlertListScrollPerformance() {
navigateToTab(.alerts)
// Wait for alerts to load
let alertsNav = app.navigationBars["Alerts"]
XCTAssertTrue(alertsNav.waitForExistence(timeout: 5))
// Wait for content to appear - populatedDashboard has 3 alerts
let alertExists = app.staticTexts["Data Exposure Detected"].waitForExistence(timeout: 5)
if !alertExists {
// May be showing empty state or loading state
return
}
// Use the scroll view in the alerts list
let scrollViews = app.scrollViews
guard scrollViews.count > 0 else { return }
measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) {
// Scroll up and down through the alert list
for _ in 0..<3 {
scrollViews.element(boundBy: 0).swipeUp()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
}
}
// MARK: - Service List Scroll Performance
/// Measures Services tab list scroll performance.
func testServiceListScrollPerformance() {
navigateToTab(.services)
let servicesNav = app.navigationBars["Services"]
XCTAssertTrue(servicesNav.waitForExistence(timeout: 5))
// Services list uses a SwiftUI List which renders as a table/collection
let tables = app.tables
guard tables.count > 0 else { return }
measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) {
for _ in 0..<4 {
tables.element(boundBy: 0).swipeUp()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
}
}
}
final class NavigationPerformanceTests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - Tab Navigation Performance
/// Measures tab bar switching performance.
/// Each navigation transition should complete in under 500ms.
func testTabNavigationPerformance() {
navigateToTab(.dashboard)
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 5))
measure(metrics: [XCTClockMetric(), XCTCPUMetric()]) {
// Navigate through all tabs in sequence
navigateToTab(.services)
let _ = app.navigationBars["Services"].waitForExistence(timeout: 3)
navigateToTab(.alerts)
let _ = app.navigationBars["Alerts"].waitForExistence(timeout: 3)
navigateToTab(.settings)
let _ = app.navigationBars["Settings"].waitForExistence(timeout: 3)
navigateToTab(.account)
let _ = app.navigationBars["Account"].waitForExistence(timeout: 3)
navigateToTab(.dashboard)
let _ = app.navigationBars["Dashboard"].waitForExistence(timeout: 3)
}
}
// MARK: - Service Detail Navigation Performance
/// Measures navigation from Services list into individual service detail views.
func testServiceDetailNavigationPerformance() {
navigateToTab(.services)
let servicesNav = app.navigationBars["Services"]
XCTAssertTrue(servicesNav.waitForExistence(timeout: 5))
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
// Tap DarkWatch service
let darkWatchButton = app.buttons["DarkWatch"]
if darkWatchButton.exists {
darkWatchButton.tap()
let _ = app.navigationBars["DarkWatch"].waitForExistence(timeout: 3)
// Navigate back
app.navigationBars.buttons.element(boundBy: 0).tap()
let _ = servicesNav.waitForExistence(timeout: 3)
}
// Tap SpamShield service
let spamShieldButton = app.buttons["SpamShield"]
if spamShieldButton.exists {
spamShieldButton.tap()
let _ = app.navigationBars["SpamShield"].waitForExistence(timeout: 3)
app.navigationBars.buttons.element(boundBy: 0).tap()
let _ = servicesNav.waitForExistence(timeout: 3)
}
}
}
}
final class DataLoadingPerformanceTests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - Dashboard Data Load Performance
/// Measures the time for dashboard data to fully load and display.
func testDashboardDataLoadPerformance() {
// Relaunch with populated data scenario
app.terminate()
app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = UITestScenario.populatedDashboard.rawValue
app.launch()
measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) {
navigateToTab(.dashboard)
// Wait for all dashboard elements to appear
let score = text("Threat Score")
let exists = score.waitForExistence(timeout: 10)
XCTAssertTrue(exists, "Dashboard data should load within timeout")
}
}
// MARK: - DarkWatch Data Load Performance
/// Measures DarkWatch service data loading time.
func testDarkWatchDataLoadPerformance() {
app.terminate()
app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = UITestScenario.darkWatchPopulated.rawValue
app.launch()
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
navigateToTab(.services)
let darkWatchButton = app.buttons["DarkWatch"]
XCTAssertTrue(darkWatchButton.waitForExistence(timeout: 5))
darkWatchButton.tap()
// Wait for watchlist items to appear
let watchlistSection = app.staticTexts["Watchlist"]
let loaded = watchlistSection.waitForExistence(timeout: 5)
XCTAssertTrue(loaded || app.staticTexts["Exposures"].waitForExistence(timeout: 3),
"DarkWatch data should load")
// Navigate back
app.navigationBars.buttons.element(boundBy: 0).tap()
}
}
}
final class MemoryPerformanceTests: UITestBase {
override class var scenario: UITestScenario { .populatedDashboard }
// MARK: - Memory Usage During Navigation
/// Measures memory usage across a full app navigation flow.
/// Baseline: should not exceed 150MB on iPhone 12.
func testMemoryUsageAcrossNavigationFlow() {
navigateToTab(.dashboard)
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 5))
measure(metrics: [XCTMemoryMetric()]) {
// Full navigation flow through the app
navigateToTab(.dashboard)
// Scroll the dashboard
let scrollView = app.scrollViews.firstMatch
if scrollView.exists {
for _ in 0..<3 { scrollView.swipeUp() }
}
navigateToTab(.services)
let _ = app.navigationBars["Services"].waitForExistence(timeout: 3)
navigateToTab(.alerts)
let _ = app.navigationBars["Alerts"].waitForExistence(timeout: 3)
navigateToTab(.settings)
let _ = app.navigationBars["Settings"].waitForExistence(timeout: 3)
navigateToTab(.dashboard)
let _ = app.navigationBars["Dashboard"].waitForExistence(timeout: 3)
}
}
// MARK: - Memory Leak Detection After Navigation
/// Verifies memory returns to baseline after navigating through views.
func testMemoryReturnsAfterNavigation() {
navigateToTab(.dashboard)
let dashboardNav = app.navigationBars["Dashboard"]
XCTAssertTrue(dashboardNav.waitForExistence(timeout: 5))
// Navigate through several views to build up state
navigateToTab(.services)
let _ = app.navigationBars["Services"].waitForExistence(timeout: 3)
// Tap into a service
let darkWatchButton = app.buttons["DarkWatch"]
if darkWatchButton.exists {
darkWatchButton.tap()
let _ = app.navigationBars["DarkWatch"].waitForExistence(timeout: 3)
// Go back
app.navigationBars.buttons.element(boundBy: 0).tap()
let _ = app.navigationBars["Services"].waitForExistence(timeout: 3)
}
navigateToTab(.alerts)
let _ = app.navigationBars["Alerts"].waitForExistence(timeout: 3)
// Return to dashboard
navigateToTab(.dashboard)
// Measure that memory is stable (no leaks)
measure(metrics: [XCTMemoryMetric()]) {
// Check that navigating again doesn't increase memory significantly
navigateToTab(.services)
let _ = app.navigationBars["Services"].waitForExistence(timeout: 3)
navigateToTab(.alerts)
let _ = app.navigationBars["Alerts"].waitForExistence(timeout: 3)
navigateToTab(.dashboard)
let _ = app.navigationBars["Dashboard"].waitForExistence(timeout: 3)
}
}
}