Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-09 22:23:44 -06:00
parent 2f85c334cb
commit 0ed1eee242
5 changed files with 135 additions and 29 deletions

View File

@ -503,7 +503,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; 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; name = Debug;
}; };
@ -525,7 +525,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; 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; name = Release;
}; };

View File

@ -21,10 +21,11 @@ struct RootView: View {
/// Whether to show the paywall (shared between views) /// Whether to show the paywall (shared between views)
@State private var showPaywall = false @State private var showPaywall = false
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
var body: some View { var body: some View {
ZStack { ZStack {
if hasCompletedOnboarding { if isScreenshotUITest || hasCompletedOnboarding {
// Main app content // Main app content
ContentView() ContentView()
.preferredColorScheme(.dark) .preferredColorScheme(.dark)

View File

@ -13,6 +13,7 @@ struct ContentView: View {
@State private var settings = SettingsViewModel() @State private var settings = SettingsViewModel()
@State private var showSettings = false @State private var showSettings = false
@State private var showPaywall = false @State private var showPaywall = false
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
@State private var capturedPhoto: CapturedPhoto? @State private var capturedPhoto: CapturedPhoto?
@State private var showPhotoReview = false @State private var showPhotoReview = false
@ -32,15 +33,25 @@ struct ContentView: View {
ZStack { ZStack {
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes // Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
if !showPhotoReview { if !showPhotoReview {
EquatableView(content: CameraContainerView( if isScreenshotUITest {
settings: settings, ScreenshotCameraPlaceholder(
sessionKey: cameraSessionKey, showSettings: $showSettings,
cameraPosition: settings.cameraPosition, ringWidth: 70.0,
onImageCaptured: { image in ringColor: .white,
handlePhotoCaptured(image) ringOpacity: 0.7
} )
)) .ignoresSafeArea()
.ignoresSafeArea() // Only camera ignores safe area to fill screen } 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 // 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 // Settings button overlay - only show when NOT in photo review mode
.overlay(alignment: .topTrailing) { .overlay(alignment: .topTrailing) {
if !showPhotoReview { if !showPhotoReview && !isScreenshotUITest {
Button { Button {
showSettings = true showSettings = true
} label: { } label: {
@ -83,6 +94,13 @@ struct ContentView: View {
.padding(.top, Design.Spacing.small) .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) .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
.onAppear { .onAppear {
// Initialize tracking of camera position // Initialize tracking of camera position
@ -220,7 +238,3 @@ struct CameraContainerView: View, Equatable {
.startSession() .startSession()
} }
} }
#Preview {
ContentView()
}

View File

@ -85,11 +85,11 @@ struct ScreenshotCameraPlaceholder: View {
} }
} }
.ignoresSafeArea() .ignoresSafeArea()
.accessibilityIdentifier("screenshot-green-screen") .accessibilityIdentifier("screenshot-camera-placeholder")
.overlay(alignment: .topLeading) { .overlay(alignment: .topLeading) {
Color.clear Color.clear
.frame(width: 1, height: 1) .frame(width: 1, height: 1)
.accessibilityIdentifier("screenshot-green-screen") .accessibilityIdentifier("screenshot-camera-placeholder")
} }
} }
} }

View File

@ -8,34 +8,125 @@
import XCTest import XCTest
final class SelfieCamUITests: XCTestCase { final class SelfieCamUITests: XCTestCase {
private var app: XCUIApplication!
override func setUpWithError() throws { 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 continueAfterFailure = false
app = XCUIApplication()
addUIInterruptionMonitor(withDescription: "System Permission Alerts") { alert in
let preferredButtons = [
"Allow",
"Allow While Using App",
"OK",
"Continue"
]
// In UI tests its 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 { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
} }
@MainActor @MainActor
func testExample() throws { func testAppStorePortraitScreenshots() throws {
// UI tests must launch the application that they test. app.launchArguments = ["-ui-testing-screenshots"]
let app = XCUIApplication() app.launchEnvironment = [
"ENABLE_DEBUG_PREMIUM": "1"
]
app.launch() 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 @MainActor
func testLaunchPerformance() throws { func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) { measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch() 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)
}
} }