// // XCUIApplication+Launch.swift // PlantGuideUITests // // Created on 2026-01-21. // import XCTest // MARK: - Launch Configuration Keys /// Keys used for launch argument and environment configuration. enum LaunchConfigKey { /// Launch arguments static let uiTesting = "-UITesting" static let cleanState = "-CleanState" static let mockData = "-MockData" static let offlineMode = "-OfflineMode" static let skipOnboarding = "-SkipOnboarding" /// Environment keys static let isUITesting = "IS_UI_TESTING" static let useMockData = "USE_MOCK_DATA" static let isOfflineMode = "IS_OFFLINE_MODE" static let mockAPIResponseDelay = "MOCK_API_RESPONSE_DELAY" } // MARK: - XCUIApplication Launch Extensions extension XCUIApplication { // MARK: - Launch Configurations /// Launches the app with a clean state, resetting all user data and preferences. /// /// Use this for tests that need a fresh start without any prior data. /// This clears: /// - All saved plants in the collection /// - Care schedules and tasks /// - User preferences and settings /// - Cached images and API responses /// /// Example: /// ```swift /// let app = XCUIApplication() /// app.launchWithCleanState() /// ``` func launchWithCleanState() { launchArguments.append(contentsOf: [ LaunchConfigKey.uiTesting, LaunchConfigKey.cleanState, LaunchConfigKey.skipOnboarding ]) launchEnvironment[LaunchConfigKey.isUITesting] = "YES" launch() } /// Launches the app with pre-populated mock data for testing. /// /// Use this for tests that need existing plants, care schedules, /// or other data to be present. The mock data includes: /// - Sample plants with various characteristics /// - Active care schedules with upcoming and overdue tasks /// - Saved user preferences /// /// - Parameter count: Number of mock plants to generate. Default is 5. /// /// Example: /// ```swift /// let app = XCUIApplication() /// app.launchWithMockData() /// ``` func launchWithMockData(plantCount: Int = 5) { launchArguments.append(contentsOf: [ LaunchConfigKey.uiTesting, LaunchConfigKey.mockData, LaunchConfigKey.skipOnboarding ]) launchEnvironment[LaunchConfigKey.isUITesting] = "YES" launchEnvironment[LaunchConfigKey.useMockData] = "YES" launchEnvironment["MOCK_PLANT_COUNT"] = String(plantCount) launch() } /// Launches the app in offline mode to simulate network unavailability. /// /// Use this for tests that verify offline behavior: /// - Cached data is displayed correctly /// - Appropriate offline indicators appear /// - Network-dependent features show proper fallback UI /// - On-device ML identification still works /// /// Example: /// ```swift /// let app = XCUIApplication() /// app.launchOffline() /// ``` func launchOffline() { launchArguments.append(contentsOf: [ LaunchConfigKey.uiTesting, LaunchConfigKey.offlineMode, LaunchConfigKey.skipOnboarding ]) launchEnvironment[LaunchConfigKey.isUITesting] = "YES" launchEnvironment[LaunchConfigKey.isOfflineMode] = "YES" launch() } /// Launches the app with custom configuration. /// /// Use this for tests requiring specific combinations of settings. /// /// - Parameters: /// - cleanState: Whether to reset all app data /// - mockData: Whether to use pre-populated test data /// - offline: Whether to simulate offline mode /// - apiDelay: Simulated API response delay in seconds (0 = instant) /// - additionalArguments: Any extra launch arguments needed /// - additionalEnvironment: Any extra environment variables needed /// /// Example: /// ```swift /// let app = XCUIApplication() /// app.launchWithConfiguration( /// mockData: true, /// apiDelay: 2.0 // Slow API to test loading states /// ) /// ``` func launchWithConfiguration( cleanState: Bool = false, mockData: Bool = false, offline: Bool = false, apiDelay: TimeInterval = 0, additionalArguments: [String] = [], additionalEnvironment: [String: String] = [:] ) { // Base arguments launchArguments.append(LaunchConfigKey.uiTesting) launchArguments.append(LaunchConfigKey.skipOnboarding) // Optional arguments if cleanState { launchArguments.append(LaunchConfigKey.cleanState) } if mockData { launchArguments.append(LaunchConfigKey.mockData) } if offline { launchArguments.append(LaunchConfigKey.offlineMode) } // Additional arguments launchArguments.append(contentsOf: additionalArguments) // Environment variables launchEnvironment[LaunchConfigKey.isUITesting] = "YES" launchEnvironment[LaunchConfigKey.useMockData] = mockData ? "YES" : "NO" launchEnvironment[LaunchConfigKey.isOfflineMode] = offline ? "YES" : "NO" if apiDelay > 0 { launchEnvironment[LaunchConfigKey.mockAPIResponseDelay] = String(apiDelay) } // Additional environment for (key, value) in additionalEnvironment { launchEnvironment[key] = value } launch() } } // MARK: - Element Waiting Extensions extension XCUIElement { /// Waits for the element to exist with a configurable timeout. /// /// - Parameter timeout: Maximum time to wait in seconds. Default is 5 seconds. /// - Returns: True if element exists within timeout, false otherwise. @discardableResult func waitForExistence(timeout: TimeInterval = 5) -> Bool { return self.waitForExistence(timeout: timeout) } /// Waits for the element to exist and be hittable. /// /// - Parameter timeout: Maximum time to wait in seconds. Default is 5 seconds. /// - Returns: True if element is hittable within timeout, false otherwise. @discardableResult func waitForHittable(timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "exists == true AND isHittable == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } /// Waits for the element to not exist (disappear). /// /// - Parameter timeout: Maximum time to wait in seconds. Default is 5 seconds. /// - Returns: True if element no longer exists within timeout, false otherwise. @discardableResult func waitForNonExistence(timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } /// Waits for the element's value to match the expected value. /// /// - Parameters: /// - expectedValue: The value to wait for. /// - timeout: Maximum time to wait in seconds. Default is 5 seconds. /// - Returns: True if element's value matches within timeout, false otherwise. @discardableResult func waitForValue(_ expectedValue: String, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "value == %@", expectedValue) let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter.wait(for: [expectation], timeout: timeout) return result == .completed } } // MARK: - App State Verification Extensions extension XCUIApplication { /// Verifies the app launched successfully by checking for the tab bar. /// /// - Parameter timeout: Maximum time to wait for the tab bar. Default is 10 seconds. /// - Returns: True if tab bar appears, false otherwise. @discardableResult func verifyLaunched(timeout: TimeInterval = 10) -> Bool { let tabBar = self.tabBars.firstMatch return tabBar.waitForExistence(timeout: timeout) } /// Navigates to a specific tab by tapping on it. /// /// - Parameter tabName: The accessible label of the tab (e.g., "Camera", "Collection"). func navigateToTab(_ tabName: String) { let tabButton = self.tabBars.buttons[tabName] if tabButton.waitForExistence(timeout: 5) { tabButton.tap() } } } // MARK: - Accessibility Identifier Constants /// Accessibility identifiers used throughout the app. /// Use these constants in tests to locate elements reliably. enum AccessibilityID { // MARK: - Tab Bar enum TabBar { static let camera = "Camera" static let collection = "Collection" static let care = "Care" static let settings = "Settings" } // MARK: - Camera View enum Camera { static let captureButton = "captureButton" static let retakeButton = "retakeButton" static let usePhotoButton = "usePhotoButton" static let permissionRequestView = "permissionRequestView" static let permissionDeniedView = "permissionDeniedView" static let cameraPreview = "cameraPreview" static let capturedImagePreview = "capturedImagePreview" } // MARK: - Collection View enum Collection { static let gridView = "collectionGridView" static let listView = "collectionListView" static let searchField = "collectionSearchField" static let filterButton = "filterButton" static let viewModeToggle = "viewModeToggle" static let emptyState = "collectionEmptyState" static let plantCell = "plantCell" static let favoriteButton = "favoriteButton" static let deleteButton = "deleteButton" } // MARK: - Settings View enum Settings { static let offlineModeToggle = "offlineModeToggle" static let clearCacheButton = "clearCacheButton" static let apiStatusSection = "apiStatusSection" static let versionLabel = "versionLabel" static let confirmClearCacheButton = "confirmClearCacheButton" } // MARK: - Plant Detail View enum PlantDetail { static let headerSection = "plantHeaderSection" static let careInfoSection = "careInformationSection" static let upcomingTasksSection = "upcomingTasksSection" static let careScheduleButton = "careScheduleButton" } // MARK: - Care Schedule View enum CareSchedule { static let taskList = "careTaskList" static let overdueSection = "overdueTasksSection" static let todaySection = "todayTasksSection" static let emptyState = "careEmptyState" static let filterButton = "careFilterButton" } }