Files
PlantGuide/PlantGuideUITests/Helpers/XCUIApplication+Launch.swift
Trey t 136dfbae33 Add PlantGuide iOS app with plant identification and care management
- Implement camera capture and plant identification workflow
- Add Core Data persistence for plants, care schedules, and cached API data
- Create collection view with grid/list layouts and filtering
- Build plant detail views with care information display
- Integrate Trefle botanical API for plant care data
- Add local image storage for captured plant photos
- Implement dependency injection container for testability
- Include accessibility support throughout the app

Bug fixes in this commit:
- Fix Trefle API decoding by removing duplicate CodingKeys
- Fix LocalCachedImage to load from correct PlantImages directory
- Set dateAdded when saving plants for proper collection sorting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:18:01 -06:00

319 lines
11 KiB
Swift

//
// 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"
}
}