// // TheNoiseClockUITests.swift // TheNoiseClockUITests // // Created by Matt Bruce on 9/7/25. // import XCTest final class TheNoiseClockUITests: XCTestCase { private enum CaptureTab: CaseIterable { case clock case alarms case noise case settings var index: Int { switch self { case .clock: 0 case .alarms: 1 case .noise: 2 case .settings: 3 } } var labels: [String] { switch self { case .clock: ["Clock", "Horloge", "Reloj"] case .alarms: ["Alarms", "Alarmes", "Alarmas"] case .noise: ["Noise", "Bruit", "Ruido"] case .settings: ["Settings", "Reglages", "Réglages", "Paramètres", "Configuracion", "Configuración", "Ajustes"] } } var symbolLabels: [String] { switch self { case .clock: ["clock", "clock.fill"] case .alarms: ["alarm", "alarm.fill"] case .noise: ["waveform", "speaker.wave.2.fill", "speaker.wave.2"] case .settings: ["gearshape", "gearshape.fill"] } } var screenIdentifier: String { switch self { case .clock: "clock.screen" case .alarms: "alarms.screen" case .noise: "noise.screen" case .settings: "settings.screen" } } } override func setUpWithError() throws { continueAfterFailure = false } @MainActor func testAppStoreScreenshots_iPhone69() throws { let app = XCUIApplication() app.launchArguments += [ "-onboarding.TheNoiseClock.hasCompletedWelcome", "YES", "-onboarding.TheNoiseClock.hasLaunched", "YES", "-uiTest.bypassAlarmKit", "YES", "-uiTest.autoPlayNoise", "YES" ] app.launch() dismissOnboardingIfNeeded(app) ensureKeepAwakeEnabled(app) captureClock(app) captureAlarmsView(app) captureNoiseView(app) captureSettingsView(app) XCUIDevice.shared.orientation = .portrait } // MARK: - Capture Steps @MainActor private func captureClock(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) ensureCapturePreconditions(app) openTab(.clock, in: app) waitForClockFullscreenTransition() saveScreenshot(named: "01-clock-view-portrait.png") XCUIDevice.shared.orientation = .landscapeLeft sleep(2) ensureCapturePreconditions(app) openTab(.clock, in: app) waitForClockFullscreenTransition() saveScreenshot(named: "02-clock-view-landscape.png") } @MainActor private func captureAlarmsView(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) ensureCapturePreconditions(app) openTab(.alarms, in: app) sleep(1) saveScreenshot(named: "03-alarms-view-portrait.png") } @MainActor private func captureNoiseView(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) ensureCapturePreconditions(app) openTab(.noise, in: app) sleep(1) saveScreenshot(named: "04-noise-view-portrait.png") } @MainActor private func captureSettingsView(_ app: XCUIApplication) { XCUIDevice.shared.orientation = .portrait sleep(1) ensureCapturePreconditions(app) openTab(.settings, in: app) sleep(1) saveScreenshot(named: "05-clock-settings-portrait.png") } @MainActor private func ensureCapturePreconditions(_ app: XCUIApplication) { ensureAlarmExistsAndEnabled(app) ensureNoiseSoundSelectedAndPlaying(app) } @MainActor private func ensureNoiseSoundSelectedAndPlaying(_ app: XCUIApplication) { openTab(.noise, in: app) guard waitForScreen(.noise, in: app) else { XCTFail("Could not navigate to noise screen before selecting a sound.") return } let playButton = noisePlayControl(in: app) guard playButton.exists else { return } if !isNoiseCurrentlyPlaying(playButton) { playButton.tap() usleep(300_000) } } private func noisePlayControl(in app: XCUIApplication) -> XCUIElement { app.descendants(matching: .any).matching(identifier: "noise.playStopButton").firstMatch } private func isNoiseCurrentlyPlaying(_ playButton: XCUIElement) -> Bool { let normalizedLabel = normalized(playButton.label) let normalizedValue = normalized((playButton.value as? String) ?? "") let stopKeywords = ["stop", "arrêter", "arreter", "detener"] let playKeywords = ["play", "lire", "jouer", "reproducir"] if stopKeywords.contains(where: { normalizedLabel.contains(normalized($0)) || normalizedValue.contains(normalized($0)) }) { return true } if playKeywords.contains(where: { normalizedLabel.contains(normalized($0)) || normalizedValue.contains(normalized($0)) }) { return false } return false } private func normalized(_ text: String) -> String { text .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current) .lowercased() } // MARK: - State Setup @MainActor private func dismissOnboardingIfNeeded(_ app: XCUIApplication) { let secondaryButton = app.buttons["onboarding.secondaryButton"] if secondaryButton.waitForExistence(timeout: 3) { secondaryButton.tap() waitForMainTabs(app) return } for _ in 0..<10 { if hasMainTabs(app) { return } let primaryButton = app.buttons["onboarding.primaryButton"] if primaryButton.waitForExistence(timeout: 1) { primaryButton.tap() usleep(300_000) continue } // Legacy fallback if accessibility identifiers are unavailable. let fallbackPrimaryLabels = ["Get Started", "Next", "Commencer", "Suivant", "Comenzar", "Siguiente"] for label in fallbackPrimaryLabels { let button = app.buttons[label] if button.exists && button.isHittable { button.tap() usleep(300_000) break } } } } @MainActor private func ensureKeepAwakeEnabled(_ app: XCUIApplication) { openTab(.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(.alarms, in: app) dismissAlarmErrorAlertIfPresent(in: app) // If no alarms exist, create one with defaults. let firstSwitch = app.switches.firstMatch if !firstSwitch.waitForExistence(timeout: 2) { tapNavigationPlusButton(in: app) let saveButton = app.buttons["alarms.add.saveButton"] XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "Add Alarm sheet did not appear.") saveButton.tap() sleep(1) dismissAlarmErrorAlertIfPresent(in: app) let cancelButton = app.buttons["alarms.add.cancelButton"] if cancelButton.exists && cancelButton.isHittable { cancelButton.tap() sleep(1) } _ = firstSwitch.waitForExistence(timeout: 3) } // Make sure at least one alarm is enabled. if firstSwitch.waitForExistence(timeout: 3), !isSwitchOn(firstSwitch) { firstSwitch.tap() sleep(1) } } // MARK: - Helpers @MainActor private func openTab(_ tab: CaptureTab, in app: XCUIApplication) { if isScreenVisible(identifier: tab.screenIdentifier, in: app) { return } 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 tapAndConfirm(_ tapAction: () -> Bool) -> Bool { guard tapAction() else { return false } return waitForScreen(tab, in: app, timeout: 1.5) } func tapLabeledButtonAnywhere() -> Bool { let matches = app.buttons.matching(NSPredicate(format: "label IN %@", tab.labels)) .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 tapSymbolButtonAnywhere() -> Bool { let matches = app.buttons.matching(NSPredicate(format: "label IN %@", tab.symbolLabels)) .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 IN %@", tab.labels)) 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 tapSymbolButton() -> Bool { let query = app.tabBars.buttons.matching(NSPredicate(format: "label IN %@", tab.symbolLabels)) 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 { let tabBar = app.tabBars.firstMatch guard tabBar.exists else { return false } let buttons = tabBar.buttons guard buttons.count > tab.index else { return false } let button = buttons.element(boundBy: tab.index) guard button.exists else { return false } button.tap() return true } if tapAndConfirm(tapIndexedButton) || tapAndConfirm(tapLabeledButton) || tapAndConfirm(tapSymbolButton) { return } // Fallback for cases where tab buttons are not exposed under tabBars. if tapAndConfirm(tapLabeledButtonAnywhere) || tapAndConfirm(tapSymbolButtonAnywhere) { 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 tapAndConfirm(tapIndexedButton) || tapAndConfirm(tapLabeledButton) || tapAndConfirm(tapSymbolButton) || tapAndConfirm(tapLabeledButtonAnywhere) || tapAndConfirm(tapSymbolButtonAnywhere) { return } dismissInterferingPromptIfPresent(in: app) if tapAndConfirm(tapIndexedButton) || tapAndConfirm(tapLabeledButton) || tapAndConfirm(tapSymbolButton) || tapAndConfirm(tapLabeledButtonAnywhere) || tapAndConfirm(tapSymbolButtonAnywhere) { return } let buttonLabels = app.buttons.allElementsBoundByIndex .prefix(16) .map(\.label) .joined(separator: ", ") XCTFail("Could not open tab '\(tab)' by label/symbol/index. tabBars=\(app.tabBars.count), buttons=[\(buttonLabels)]") } @MainActor private func tapNavigationPlusButton(in app: XCUIApplication) { let plusCandidates = [ app.buttons["alarms.addButton"], 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.") } @MainActor private func dismissInterferingPromptIfPresent(in app: XCUIApplication) { guard app.tabBars.count == 0 else { return } let allButtons = app.buttons.allElementsBoundByIndex.filter(\.exists) guard allButtons.count >= 2 else { return } // Keep Awake prompt can obscure tabs in some locale/device combinations. let candidate = allButtons[1] if candidate.isHittable { candidate.tap() } else { let coordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) .withOffset(CGVector(dx: candidate.frame.midX, dy: candidate.frame.midY)) coordinate.tap() } usleep(300_000) } 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) } } @MainActor private func dismissAlarmErrorAlertIfPresent(in app: XCUIApplication) { let alert = app.alerts.firstMatch guard alert.waitForExistence(timeout: 1) else { return } let dismissLabels = ["OK", "D’accord", "Aceptar", "Close", "Fermer", "Cerrar"] for label in dismissLabels { let button = alert.buttons[label] if button.exists && button.isHittable { button.tap() usleep(250_000) return } } let fallback = alert.buttons.firstMatch if fallback.exists { fallback.tap() usleep(250_000) } } private func waitForScreen(_ tab: CaptureTab, in app: XCUIApplication, timeout: TimeInterval = 2) -> Bool { let attempts = max(1, Int(timeout / 0.2)) for _ in 0.. Bool { let match = app.descendants(matching: .any).matching(identifier: identifier).firstMatch return match.exists } private func isTabSelected(_ tab: CaptureTab, in app: XCUIApplication) -> Bool { let tabBar = app.tabBars.firstMatch guard tabBar.exists else { return false } let buttons = tabBar.buttons guard buttons.count > tab.index else { return false } let button = buttons.element(boundBy: tab.index) if button.isSelected { return true } let value = normalized((button.value as? String) ?? "") return value == "1" || value.contains("selected") } private func hasMainTabs(_ app: XCUIApplication) -> Bool { let labels = CaptureTab.allCases .flatMap(\.labels) .appending(contentsOf: CaptureTab.allCases.flatMap(\.symbolLabels)) if app.tabBars.count > 0 { return true } return labels.contains(where: { app.buttons[$0].exists }) } } private extension Array { func appending(contentsOf elements: [Element]) -> [Element] { var copy = self copy.append(contentsOf: elements) return copy } }