- 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
355 lines
13 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|