// // CameraFlowUITests.swift // PlantGuideUITests // // Created on 2026-01-21. // // UI tests for the camera and plant identification flow including // permission handling, capture, and photo preview. // import XCTest final class CameraFlowUITests: XCTestCase { // MARK: - Properties var app: XCUIApplication! // MARK: - Setup & Teardown override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() } override func tearDownWithError() throws { app = nil } // MARK: - Permission Request Tests /// Tests that camera permission request view appears for new users. /// /// Note: This test assumes the app is launched in a state where /// camera permission has not been determined. The actual system /// permission dialog behavior depends on the device state. @MainActor func testCameraPermissionRequestViewAppears() throws { // Given: App launched with clean state (permission not determined) app.launchWithCleanState() // When: App is on Camera tab (default tab) // The Camera tab should be selected by default based on MainTabView // Then: Permission request view should display for new users // Look for the permission request UI elements let permissionTitle = app.staticTexts["Camera Access Required"] let permissionDescription = app.staticTexts.matching( NSPredicate(format: "label CONTAINS[c] 'camera access'") ).firstMatch // Give time for the permission request view to appear let titleExists = permissionTitle.waitForExistence(timeout: 5) let descriptionExists = permissionDescription.waitForExistence(timeout: 2) // At least one of these elements should exist if permission is not determined // or the camera view itself if already authorized let cameraIcon = app.images.matching( NSPredicate(format: "identifier == 'camera.fill' OR label CONTAINS[c] 'camera'") ).firstMatch XCTAssertTrue( titleExists || descriptionExists || cameraIcon.waitForExistence(timeout: 2), "Camera permission request view or camera UI should appear" ) } /// Tests that the permission denied view shows appropriate messaging. /// /// Note: This test verifies the UI elements that appear when camera /// access is denied. Actual permission state cannot be controlled in UI tests. @MainActor func testCameraPermissionDeniedViewElements() throws { // Given: App launched (permission state depends on device) app.launchWithCleanState() // When: Camera permission is denied (if in denied state) // We check for the presence of permission denied UI elements // Then: Look for denied state elements let deniedTitle = app.staticTexts["Camera Access Denied"] let openSettingsButton = app.buttons["Open Settings"] // These will exist only if permission is actually denied // We verify the test setup is correct if deniedTitle.waitForExistence(timeout: 3) { XCTAssertTrue(deniedTitle.exists, "Denied title should be visible") XCTAssertTrue( openSettingsButton.waitForExistence(timeout: 2), "Open Settings button should be visible when permission denied" ) // Verify the description text let description = app.staticTexts.matching( NSPredicate(format: "label CONTAINS[c] 'enable camera access in Settings'") ).firstMatch XCTAssertTrue(description.exists, "Description should explain how to enable camera") } } // MARK: - Capture Button Tests /// Tests that the capture button exists when camera is authorized. /// /// Note: This test assumes camera permission has been granted. /// The test will check for the capture button's presence. @MainActor func testCaptureButtonExistsWhenAuthorized() throws { // Given: App launched (assuming camera permission granted) app.launchWithMockData() // When: Navigate to Camera tab (or stay if default) app.navigateToTab(AccessibilityID.TabBar.camera) // Then: Look for capture button (circular button with specific accessibility) let captureButton = app.buttons["Capture photo"] // If camera is authorized, capture button should exist // If not authorized, we skip the assertion if captureButton.waitForExistence(timeout: 5) { XCTAssertTrue(captureButton.exists, "Capture button should exist when camera authorized") XCTAssertTrue(captureButton.isEnabled, "Capture button should be enabled") } else { // Camera might not be authorized - check for permission views let permissionView = app.staticTexts["Camera Access Required"].exists || app.staticTexts["Camera Access Denied"].exists XCTAssertTrue(permissionView, "Should show either capture button or permission view") } } /// Tests capture button has correct accessibility label and hint. @MainActor func testCaptureButtonAccessibility() throws { // Given: App launched with camera access app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.camera) // When: Capture button is available let captureButton = app.buttons["Capture photo"] if captureButton.waitForExistence(timeout: 5) { // Then: Check accessibility properties XCTAssertEqual( captureButton.label, "Capture photo", "Capture button should have correct accessibility label" ) } } // MARK: - Photo Preview Tests /// Tests that photo preview appears after capture (mock scenario). /// /// Note: In UI tests, we cannot actually trigger a real camera capture. /// This test verifies the preview UI when the app is in the appropriate state. @MainActor func testPhotoPreviewUIElements() throws { // Given: App launched app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.camera) // Check if capture button exists (camera authorized) let captureButton = app.buttons["Capture photo"] if captureButton.waitForExistence(timeout: 5) { // When: Capture button is tapped // Note: This may not actually capture in simulator without mock captureButton.tap() // Then: Either capturing overlay or preview should appear // Look for capturing state let capturingText = app.staticTexts["Capturing..."] let retakeButton = app.buttons["Retake photo"] let usePhotoButton = app.buttons["Use this photo"] // Wait for either capturing state or preview to appear let capturingAppeared = capturingText.waitForExistence(timeout: 3) let previewAppeared = retakeButton.waitForExistence(timeout: 5) || usePhotoButton.waitForExistence(timeout: 2) // In a mocked environment, one of these states should occur // If camera isn't available, we just verify no crash occurred XCTAssertTrue( capturingAppeared || previewAppeared || captureButton.exists, "App should handle capture attempt gracefully" ) } } /// Tests that retake button is functional in preview mode. @MainActor func testRetakeButtonInPreview() throws { // Given: App with potential captured image state app.launchWithConfiguration(mockData: true, additionalEnvironment: [ "MOCK_CAPTURED_IMAGE": "YES" ]) app.navigateToTab(AccessibilityID.TabBar.camera) // Look for retake button (indicates preview state) let retakeButton = app.buttons["Retake photo"] if retakeButton.waitForExistence(timeout: 5) { // When: Retake button exists and is tapped XCTAssertTrue(retakeButton.isEnabled, "Retake button should be enabled") retakeButton.tap() // Then: Should return to camera view let captureButton = app.buttons["Capture photo"] XCTAssertTrue( captureButton.waitForExistence(timeout: 5), "Should return to camera view after retake" ) } } /// Tests that "Use Photo" button is present in preview mode. @MainActor func testUsePhotoButtonInPreview() throws { // Given: App with potential captured image state app.launchWithConfiguration(mockData: true, additionalEnvironment: [ "MOCK_CAPTURED_IMAGE": "YES" ]) app.navigateToTab(AccessibilityID.TabBar.camera) // Look for use photo button (indicates preview state) let usePhotoButton = app.buttons["Use this photo"] if usePhotoButton.waitForExistence(timeout: 5) { // Then: Use Photo button should have correct properties XCTAssertTrue(usePhotoButton.isEnabled, "Use Photo button should be enabled") // Check for the prompt text let promptText = app.staticTexts["Ready to identify this plant?"] XCTAssertTrue(promptText.exists, "Prompt text should appear above Use Photo button") } } // MARK: - Camera View State Tests /// Tests camera view handles different permission states gracefully. @MainActor func testCameraViewStateHandling() throws { // Given: App launched app.launchWithCleanState() // When: Camera tab is displayed app.navigateToTab(AccessibilityID.TabBar.camera) // Then: One of three states should be visible: // 1. Permission request (not determined) // 2. Permission denied // 3. Camera preview with capture button let permissionRequest = app.staticTexts["Camera Access Required"] let permissionDenied = app.staticTexts["Camera Access Denied"] let captureButton = app.buttons["Capture photo"] let hasValidState = permissionRequest.waitForExistence(timeout: 3) || permissionDenied.waitForExistence(timeout: 2) || captureButton.waitForExistence(timeout: 2) XCTAssertTrue(hasValidState, "Camera view should show a valid state") } /// Tests that camera controls are disabled during capture. @MainActor func testCameraControlsDisabledDuringCapture() throws { // Given: App with camera access app.launchWithConfiguration(mockData: true, additionalEnvironment: [ "MOCK_API_RESPONSE_DELAY": "3" // Slow response to observe disabled state ]) app.navigateToTab(AccessibilityID.TabBar.camera) let captureButton = app.buttons["Capture photo"] if captureButton.waitForExistence(timeout: 5) && captureButton.isEnabled { // When: Capture is initiated captureButton.tap() // Then: During capture, controls may be disabled // Look for capturing overlay let capturingOverlay = app.staticTexts["Capturing..."] if capturingOverlay.waitForExistence(timeout: 2) { // Verify UI shows capturing state XCTAssertTrue(capturingOverlay.exists, "Capturing indicator should be visible") } } } // MARK: - Error Handling Tests /// Tests that camera errors are displayed to the user. @MainActor func testCameraErrorAlert() throws { // Given: App launched app.launchWithMockData() app.navigateToTab(AccessibilityID.TabBar.camera) // Error alerts are shown via .alert modifier // We verify the alert can be dismissed if it appears let errorAlert = app.alerts["Error"] if errorAlert.waitForExistence(timeout: 3) { // Then: Error alert should have OK button to dismiss let okButton = errorAlert.buttons["OK"] XCTAssertTrue(okButton.exists, "Error alert should have OK button") okButton.tap() // Alert should dismiss XCTAssertTrue( errorAlert.waitForNonExistence(timeout: 2), "Error alert should dismiss after tapping OK" ) } } // MARK: - Navigation Tests /// Tests that camera tab is the default selected tab. @MainActor func testCameraTabIsDefault() throws { // Given: App freshly launched app.launchWithCleanState() // Then: Camera tab should be selected let cameraTab = app.tabBars.buttons[AccessibilityID.TabBar.camera] XCTAssertTrue(cameraTab.waitForExistence(timeout: 5), "Camera tab should exist") XCTAssertTrue(cameraTab.isSelected, "Camera tab should be selected by default") } /// Tests navigation from camera to identification flow. @MainActor func testNavigationToIdentificationFlow() throws { // Given: App with captured image ready app.launchWithConfiguration(mockData: true, additionalEnvironment: [ "MOCK_CAPTURED_IMAGE": "YES" ]) app.navigateToTab(AccessibilityID.TabBar.camera) // Look for use photo button let usePhotoButton = app.buttons["Use this photo"] if usePhotoButton.waitForExistence(timeout: 5) { // When: Use Photo is tapped usePhotoButton.tap() // Then: Should navigate to identification view (full screen cover) // The identification view might show loading or results let identificationView = app.otherElements.matching( NSPredicate(format: "identifier CONTAINS[c] 'identification'") ).firstMatch // Or look for common identification view elements let loadingText = app.staticTexts.matching( NSPredicate(format: "label CONTAINS[c] 'identifying' OR label CONTAINS[c] 'analyzing'") ).firstMatch let viewAppeared = identificationView.waitForExistence(timeout: 5) || loadingText.waitForExistence(timeout: 3) // If mock data doesn't trigger full flow, just verify no crash XCTAssertTrue( viewAppeared || app.exists, "App should handle navigation to identification" ) } } }