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

226 lines
7.4 KiB
Swift

import XCTest
/// Mock scenarios matching the main app's UITestScenario values.
/// These are passed via launch environment to configure app behavior.
enum UITestScenario: String, CaseIterable {
case emptyDashboard
case populatedDashboard
case authenticated
case unauthenticated
case darkWatchPopulated
case voicePrintPopulated
case spamShieldPopulated
case homeTitlePopulated
case removeBrokersPopulated
case settingsPopulated
case authError
case signupSuccess
case forgotPasswordSuccess
}
/// Base class for all Kordant UI tests.
/// Provides common setup, teardown, and helper methods.
class UITestBase: XCTestCase {
var app: XCUIApplication!
/// The mock scenario to use for this test class.
/// Subclasses can override to change the scenario.
class var scenario: UITestScenario { .populatedDashboard }
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// Configure testing mode via launch arguments and environment
app.launchArguments = ["-UITesting"]
app.launchEnvironment["UITestScenario"] = Self.scenario.rawValue
// Disable animations for faster, more reliable tests
app.launchEnvironment["UITestDisableAnimations"] = "YES"
// Reset any persisted state for clean test runs
app.launch()
// Wait for the app to settle after launch
let _ = app.wait(for: .runningForeground, timeout: 5)
}
override func tearDownWithError() throws {
// Capture screenshot on failure
if testRun?.hasSucceeded == false {
let screenshot = app.windows.firstMatch.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Failure - \(name)"
attachment.lifetime = .keepAlways
add(attachment)
}
app = nil
super.tearDownWithError()
}
// MARK: - Navigation Helpers
/// Navigate to a specific tab in the main tab bar
func navigateToTab(_ tab: TabBarItem) {
app.tabBars.buttons[tab.rawValue].tap()
}
enum TabBarItem: String {
case dashboard = "Dashboard"
case services = "Services"
case alerts = "Alerts"
case settings = "Settings"
case account = "Account"
}
// MARK: - Element Queries
/// Find a button by its accessibility label or title
func button(_ label: String) -> XCUIElement {
app.buttons[label]
}
/// Find a text field by its placeholder or label
func textField(_ label: String) -> XCUIElement {
let field = app.textFields[label]
if field.exists { return field }
return app.textFields.containing(.staticText, identifier: label).element
}
/// Find a secure text field by its placeholder or label
func secureTextField(_ label: String) -> XCUIElement {
let field = app.secureTextFields[label]
if field.exists { return field }
return app.secureTextFields.containing(.staticText, identifier: label).element
}
/// Find static text by label
func text(_ label: String) -> XCUIElement {
app.staticTexts[label]
}
/// Find a navigation bar by its title
func navigationBar(_ title: String) -> XCUIElement {
app.navigationBars[title]
}
/// Find a toggle/switch by its label
func `switch`(_ label: String) -> XCUIElement {
app.switches[label]
}
// MARK: - Waiting Helpers
/// Wait for an element to appear with a timeout
@discardableResult
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
/// Wait for an element to be hittable (visible and interactable)
@discardableResult
func waitForHittable(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "hittable == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
/// Wait for an element to disappear
@discardableResult
func waitForAbsence(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
return result == .completed
}
// MARK: - Gesture Helpers
/// Pull to refresh on a scroll view
func pullToRefresh() {
let window = app.windows.firstMatch
let start = window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
let end = window.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6))
start.press(forDuration: 0, thenDragTo: end)
}
/// Scroll down in a scroll view
func scrollDown(times: Int = 1) {
for _ in 0..<times {
app.swipeUp()
}
}
/// Scroll up in a scroll view
func scrollUp(times: Int = 1) {
for _ in 0..<times {
app.swipeDown()
}
}
// MARK: - Auth Helpers
/// Type credentials into the login form
func typeLoginCredentials(email: String = "test@kordant.com", password: String = "TestPassword123") {
let emailField = app.textFields["Email"]
guard emailField.waitForExistence(timeout: 3) else { return }
emailField.tap()
emailField.typeText(email)
let passwordField = app.secureTextFields["Password"]
passwordField.tap()
passwordField.typeText(password)
}
/// Type credentials into the signup form
func typeSignupCredentials(name: String = "Test User", email: String = "test@kordant.com", password: String = "TestPassword123") {
let nameField = app.textFields.firstMatch
guard nameField.waitForExistence(timeout: 3) else { return }
nameField.tap()
nameField.typeText(name)
let emailField = app.textFields.element(boundBy: 1)
emailField.tap()
emailField.typeText(email)
let passwordField = app.secureTextFields.firstMatch
passwordField.tap()
passwordField.typeText(password)
// Confirm password field
let confirmField = app.secureTextFields.element(boundBy: 1)
if confirmField.exists {
confirmField.tap()
confirmField.typeText(password)
}
}
/// Dismiss the keyboard if present
func dismissKeyboard() {
if app.keyboards.element(boundBy: 0).exists {
app.toolbars.buttons["Done"].tap()
// Fallback: tap a non-interactive area
if app.keyboards.element(boundBy: 0).exists {
app.staticTexts.firstMatch.tap()
}
}
}
/// Take a screenshot and attach it to the test report
func captureScreen(name: String = #function) {
let screenshot = app.windows.firstMatch.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}