diff --git a/SelfieCam.xcodeproj/project.pbxproj b/SelfieCam.xcodeproj/project.pbxproj index fac7f2d..8a4363a 100644 --- a/SelfieCam.xcodeproj/project.pbxproj +++ b/SelfieCam.xcodeproj/project.pbxproj @@ -503,7 +503,7 @@ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam"; }; name = Debug; }; @@ -525,7 +525,7 @@ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam"; }; name = Release; }; diff --git a/SelfieCam/App/RootView.swift b/SelfieCam/App/RootView.swift index 5739ae8..a186c17 100644 --- a/SelfieCam/App/RootView.swift +++ b/SelfieCam/App/RootView.swift @@ -21,10 +21,11 @@ struct RootView: View { /// Whether to show the paywall (shared between views) @State private var showPaywall = false + private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots") var body: some View { ZStack { - if hasCompletedOnboarding { + if isScreenshotUITest || hasCompletedOnboarding { // Main app content ContentView() .preferredColorScheme(.dark) diff --git a/SelfieCam/Features/Camera/Views/ContentView.swift b/SelfieCam/Features/Camera/Views/ContentView.swift index 141dca3..07a15de 100644 --- a/SelfieCam/Features/Camera/Views/ContentView.swift +++ b/SelfieCam/Features/Camera/Views/ContentView.swift @@ -13,6 +13,7 @@ struct ContentView: View { @State private var settings = SettingsViewModel() @State private var showSettings = false @State private var showPaywall = false + private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots") @State private var capturedPhoto: CapturedPhoto? @State private var showPhotoReview = false @@ -32,15 +33,25 @@ struct ContentView: View { ZStack { // Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes if !showPhotoReview { - EquatableView(content: CameraContainerView( - settings: settings, - sessionKey: cameraSessionKey, - cameraPosition: settings.cameraPosition, - onImageCaptured: { image in - handlePhotoCaptured(image) - } - )) - .ignoresSafeArea() // Only camera ignores safe area to fill screen + if isScreenshotUITest { + ScreenshotCameraPlaceholder( + showSettings: $showSettings, + ringWidth: 70.0, + ringColor: .white, + ringOpacity: 0.7 + ) + .ignoresSafeArea() + } else { + EquatableView(content: CameraContainerView( + settings: settings, + sessionKey: cameraSessionKey, + cameraPosition: settings.cameraPosition, + onImageCaptured: { image in + handlePhotoCaptured(image) + } + )) + .ignoresSafeArea() // Only camera ignores safe area to fill screen + } } // Photo review overlay - handles its own safe area @@ -67,7 +78,7 @@ struct ContentView: View { ) // Settings button overlay - only show when NOT in photo review mode .overlay(alignment: .topTrailing) { - if !showPhotoReview { + if !showPhotoReview && !isScreenshotUITest { Button { showSettings = true } label: { @@ -83,6 +94,13 @@ struct ContentView: View { .padding(.top, Design.Spacing.small) } } + .overlay(alignment: .topLeading) { + if isScreenshotUITest { + Color.clear + .frame(width: 1, height: 1) + .accessibilityIdentifier("screenshot-mode-active") + } + } .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview) .onAppear { // Initialize tracking of camera position @@ -220,7 +238,3 @@ struct CameraContainerView: View, Equatable { .startSession() } } - -#Preview { - ContentView() -} diff --git a/SelfieCam/Features/Camera/Views/ScreenshotCameraPlaceholder.swift b/SelfieCam/Features/Camera/Views/ScreenshotCameraPlaceholder.swift index 1b72c8d..6534fd3 100644 --- a/SelfieCam/Features/Camera/Views/ScreenshotCameraPlaceholder.swift +++ b/SelfieCam/Features/Camera/Views/ScreenshotCameraPlaceholder.swift @@ -85,11 +85,11 @@ struct ScreenshotCameraPlaceholder: View { } } .ignoresSafeArea() - .accessibilityIdentifier("screenshot-green-screen") + .accessibilityIdentifier("screenshot-camera-placeholder") .overlay(alignment: .topLeading) { Color.clear .frame(width: 1, height: 1) - .accessibilityIdentifier("screenshot-green-screen") + .accessibilityIdentifier("screenshot-camera-placeholder") } } } diff --git a/SelfieCamUITests/SelfieCamUITests.swift b/SelfieCamUITests/SelfieCamUITests.swift index 406bc4a..c0702dd 100644 --- a/SelfieCamUITests/SelfieCamUITests.swift +++ b/SelfieCamUITests/SelfieCamUITests.swift @@ -8,34 +8,125 @@ import XCTest final class SelfieCamUITests: XCTestCase { + private var app: XCUIApplication! override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false + app = XCUIApplication() + addUIInterruptionMonitor(withDescription: "System Permission Alerts") { alert in + let preferredButtons = [ + "Allow", + "Allow While Using App", + "OK", + "Continue" + ] - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + for title in preferredButtons where alert.buttons[title].exists { + alert.buttons[title].tap() + return true + } + + if let firstButton = alert.buttons.allElementsBoundByIndex.first { + firstButton.tap() + return true + } + + return false + } } override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. } @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() + func testAppStorePortraitScreenshots() throws { + app.launchArguments = ["-ui-testing-screenshots"] + app.launchEnvironment = [ + "ENABLE_DEBUG_PREMIUM": "1" + ] app.launch() + if !waitForCameraScreen(timeout: 8) { + app.terminate() + app.launch() + } - // Use XCTAssert and related functions to verify your tests produce the correct results. + saveScreenshot(named: "01-launch-screen") + XCTAssertTrue(waitForCameraScreen(timeout: 25), "Camera screen did not appear") + XCTAssertTrue(app.otherElements["screenshot-camera-placeholder"].waitForExistence(timeout: 5), "Screenshot camera placeholder did not appear") + RunLoop.current.run(until: Date().addingTimeInterval(0.6)) + saveScreenshot(named: "02-camera-ring-light") + + let settingsButton = app.buttons["Settings"] + XCTAssertTrue(settingsButton.waitForExistence(timeout: 10), "Settings button not found") + settingsButton.tap() + XCTAssertTrue(app.otherElements["settings-sheet"].waitForExistence(timeout: 8), "Settings sheet did not appear") + RunLoop.current.run(until: Date().addingTimeInterval(0.8)) + saveScreenshot(named: "03-settings-popup") } @MainActor func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() } } + + // MARK: - Helpers + + @MainActor + private func waitForCameraScreen(timeout: TimeInterval) -> Bool { + let onboardingActions = [ + "Get Started", + "Continue", + "Enable Camera", + "Enable Microphone", + "Enable Photos", + "Done" + ] + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if app.otherElements["screenshot-mode-active"].exists { + return true + } + _ = tapFirstExistingButton(labels: onboardingActions) + tapFirstSpringboardButton(labels: ["Allow", "Allow While Using App", "OK", "Continue"]) + if app.alerts.firstMatch.exists { + app.tap() + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) + } + return app.otherElements["screenshot-mode-active"].exists + } + + @MainActor + private func tapFirstSpringboardButton(labels: [String]) { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + for label in labels { + let button = springboard.buttons[label] + if button.waitForExistence(timeout: 0.2) { + button.tap() + return + } + } + } + + @MainActor + private func tapFirstExistingButton(labels: [String]) -> Bool { + for label in labels { + let button = app.buttons[label] + if button.exists { + button.tap() + return true + } + } + return false + } + + @MainActor + private func saveScreenshot(named name: String) { + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } }