// // TheNoiseClockUITests.swift // TheNoiseClockUITests // // Created by Matt Bruce on 9/7/25. // import XCTest final class TheNoiseClockUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } @MainActor func testAppStoreScreenshots_iPhone69() throws { let app = XCUIApplication() app.launchArguments += [ "-onboarding.TheNoiseClock.hasCompletedWelcome", "YES", "-onboarding.TheNoiseClock.hasLaunched", "YES" ] app.launch() dismissOnboardingIfNeeded(app) ensureKeepAwakeEnabled(app) ensureAlarmExistsAndEnabled(app) captureNoiseViewPortraitAndLandscape(app) captureClockPortraitAndLandscape(app) captureAlarmsViewPortraitAndLandscape(app) captureSettingsViewPortraitAndLandscape(app) XCUIDevice.shared.orientation = .portrait } // MARK: - Capture Steps @MainActor private func captureClockPortraitAndLandscape(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) openTab(named: "Clock", in: app) waitForClockFullscreenTransition() saveScreenshot(named: "01-clock-view-portrait.png") XCUIDevice.shared.orientation = .landscapeLeft sleep(2) openTab(named: "Clock", in: app) waitForClockFullscreenTransition() saveScreenshot(named: "02-clock-view-landscape.png") } @MainActor private func captureAlarmsViewPortraitAndLandscape(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) openTab(named: "Alarms", in: app) sleep(1) saveScreenshot(named: "03-alarms-view-portrait.png") XCUIDevice.shared.orientation = .landscapeLeft sleep(2) openTab(named: "Alarms", in: app) sleep(1) saveScreenshot(named: "04-alarms-view-landscape.png") } @MainActor private func captureNoiseViewPortraitAndLandscape(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) openTab(named: "Noise", in: app) ensureNoiseSoundSelectedAndPlaying(app) sleep(1) saveScreenshot(named: "05-noise-view-portrait.png") XCUIDevice.shared.orientation = .landscapeLeft sleep(2) openTab(named: "Noise", in: app) ensureNoiseSoundSelectedAndPlaying(app) sleep(1) saveScreenshot(named: "06-noise-view-landscape.png") } @MainActor private func captureSettingsViewPortraitAndLandscape(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) openTab(named: "Settings", in: app) sleep(1) saveScreenshot(named: "07-clock-settings-portrait.png") XCUIDevice.shared.orientation = .landscapeLeft sleep(2) openTab(named: "Settings", in: app) sleep(1) saveScreenshot(named: "08-clock-settings-landscape.png") } @MainActor private func ensureNoiseSoundSelectedAndPlaying(_ app: XCUIApplication) { // Select a known sound so controls become visible. let soundNames = [ "White Noise", "Brown Noise", "Heavy Rain", "Atmospheric Pad", "Fan Heater" ] var didSelectSound = false for _ in 0..<8 { for soundName in soundNames { let sound = app.staticTexts[soundName] if sound.exists && sound.isHittable { sound.tap() didSelectSound = true break } } if didSelectSound { break } app.swipeUp() usleep(250_000) } let playButton = app.buttons["Play Sound"] if playButton.waitForExistence(timeout: 4) { playButton.tap() } } // MARK: - State Setup @MainActor private func dismissOnboardingIfNeeded(_ app: XCUIApplication) { let secondaryButton = app.buttons["onboarding.secondaryButton"] if secondaryButton.waitForExistence(timeout: 3), secondaryButton.label == "Skip" { secondaryButton.tap() waitForMainTabs(app) return } if app.buttons["Skip"].waitForExistence(timeout: 3) { app.buttons["Skip"].tap() waitForMainTabs(app) return } // Fallback: walk onboarding pages if skip is not available. for _ in 0..<6 { if hasMainTabs(app) { return } let next = app.buttons["Next"] let getStarted = app.buttons["Get Started"] if getStarted.exists { getStarted.tap() waitForMainTabs(app) return } if next.exists { next.tap() usleep(300_000) } } } @MainActor private func ensureKeepAwakeEnabled(_ app: XCUIApplication) { openTab(named: "Settings", in: app) let keepAwakeSwitch = app.switches["settings.keepAwake.toggle"] for _ in 0..<8 { if keepAwakeSwitch.exists { break } app.swipeUp() usleep(200_000) } guard keepAwakeSwitch.waitForExistence(timeout: 3) else { XCTFail("Could not find Keep Awake toggle by accessibility identifier.") return } if !isSwitchOn(keepAwakeSwitch) { keepAwakeSwitch.tap() sleep(1) } } @MainActor private func ensureAlarmExistsAndEnabled(_ app: XCUIApplication) { openTab(named: "Alarms", in: app) // If no alarms exist, create one with defaults. if app.staticTexts["No Alarms Set"].exists || app.buttons["Add Your First Alarm"].exists { let addFirstAlarm = app.buttons["Add Your First Alarm"] if addFirstAlarm.exists { addFirstAlarm.tap() } else { tapNavigationPlusButton(in: app) } let saveButton = app.buttons["Save"] XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "Add Alarm sheet did not appear.") saveButton.tap() sleep(1) } // Make sure at least one alarm is enabled. let firstSwitch = app.switches.firstMatch if firstSwitch.waitForExistence(timeout: 3), !isSwitchOn(firstSwitch) { firstSwitch.tap() sleep(1) } } // MARK: - Helpers @MainActor private func openTab(named tabName: String, in app: XCUIApplication) { let expectedIndex: Int? = switch tabName { case "Clock": 0 case "Alarms": 1 case "Noise": 2 case "Settings": 3 default: nil } func tapByCoordinates(_ element: XCUIElement) { let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) .withOffset(CGVector(dx: element.frame.midX, dy: element.frame.midY)) coordinate.tap() } func tapLabeledButtonAnywhere() -> Bool { let matches = app.buttons.matching(NSPredicate(format: "label == %@", tabName)) .allElementsBoundByIndex .filter(\.exists) guard !matches.isEmpty else { return false } if let hittable = matches.first(where: \.isHittable) { hittable.tap() return true } tapByCoordinates(matches[0]) return true } func tapLabeledButton() -> Bool { let query = app.tabBars.buttons.matching(NSPredicate(format: "label == %@", tabName)) let matches = query.allElementsBoundByIndex.filter(\.exists) guard !matches.isEmpty else { return false } if let hittable = matches.first(where: \.isHittable) { hittable.tap() return true } tapByCoordinates(matches[0]) return true } func tapIndexedButton() -> Bool { guard let expectedIndex else { return false } let tabBar = app.tabBars.firstMatch guard tabBar.exists else { return false } let buttons = tabBar.buttons guard buttons.count > expectedIndex else { return false } let button = buttons.element(boundBy: expectedIndex) guard button.exists else { return false } button.tap() return true } if tapLabeledButton() || tapIndexedButton() { return } // Fallback for cases where tab buttons are not exposed under tabBars. if tapLabeledButtonAnywhere() { return } // Reveal tab bar when clock is in auto full-screen mode. app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() usleep(300_000) app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.98)).tap() usleep(300_000) if tapLabeledButton() || tapIndexedButton() || tapLabeledButtonAnywhere() { return } let buttonLabels = app.buttons.allElementsBoundByIndex .prefix(16) .map(\.label) .joined(separator: ", ") XCTFail("Could not open tab '\(tabName)' by label or index. tabBars=\(app.tabBars.count), buttons=[\(buttonLabels)]") } @MainActor private func tapNavigationPlusButton(in app: XCUIApplication) { let plusCandidates = [ app.navigationBars.buttons["plus"], app.navigationBars.buttons["Add"], app.buttons["plus"] ] for candidate in plusCandidates where candidate.exists { candidate.tap() return } XCTFail("Could not find add (+) button.") } private func isSwitchOn(_ toggle: XCUIElement) -> Bool { guard let value = toggle.value as? String else { return false } return value == "1" || value.lowercased() == "on" } private func waitForClockFullscreenTransition() { // Clock auto-hides chrome after 5 seconds of inactivity. sleep(6) } private func saveScreenshot(named fileName: String) { let screenshot = XCUIScreen.main.screenshot() let data = screenshot.pngRepresentation let destination = screenshotDirectory().appendingPathComponent(fileName) do { try data.write(to: destination) } catch { XCTFail("Failed to write screenshot to \(destination.path): \(error)") } let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.png") attachment.name = fileName attachment.lifetime = .keepAlways add(attachment) } private func screenshotDirectory() -> URL { let env = ProcessInfo.processInfo.environment["SCREENSHOT_OUTPUT_DIR"] let path = (env?.isEmpty == false) ? env! : NSTemporaryDirectory() let url = URL(fileURLWithPath: path, isDirectory: true) try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url } private func waitForMainTabs(_ app: XCUIApplication) { for _ in 0..<20 { if hasMainTabs(app) { return } usleep(200_000) } } private func hasMainTabs(_ app: XCUIApplication) -> Bool { let labels = ["Clock", "Alarms", "Noise", "Settings"] if app.tabBars.count > 0 { return true } return labels.contains(where: { app.buttons[$0].exists }) } }