From 86e4382cc2e98419f0fc8615bc49c238e8890cce Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 8 Feb 2026 09:33:51 -0600 Subject: [PATCH] updated tests Signed-off-by: Matt Bruce --- .../TheNoiseClockUITests.swift | 375 +++++++++++++++++- 1 file changed, 359 insertions(+), 16 deletions(-) diff --git a/TheNoiseClockUITests/TheNoiseClockUITests.swift b/TheNoiseClockUITests/TheNoiseClockUITests.swift index cc85de5..c428301 100644 --- a/TheNoiseClockUITests/TheNoiseClockUITests.swift +++ b/TheNoiseClockUITests/TheNoiseClockUITests.swift @@ -10,32 +10,375 @@ import XCTest final class TheNoiseClockUITests: XCTestCase { 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 - - // 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. - } - - 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. + func testAppStoreScreenshots_iPhone69() throws { let app = XCUIApplication() + app.launchArguments += [ + "-onboarding.TheNoiseClock.hasCompletedWelcome", "YES", + "-onboarding.TheNoiseClock.hasLaunched", "YES" + ] app.launch() - // Use XCTAssert and related functions to verify your tests produce the correct results. + 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 - func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() + 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) { + 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 keepAwakeLabel = app.staticTexts["Keep Awake"] + for _ in 0..<8 { + if keepAwakeLabel.exists { break } + app.swipeUp() + usleep(200_000) + } + + guard keepAwakeLabel.waitForExistence(timeout: 3) else { + XCTFail("Could not find Keep Awake toggle in Settings.") + return + } + + let labelY = keepAwakeLabel.frame.midY + let switchCandidates = app.switches.allElementsBoundByIndex.filter { element in + abs(element.frame.midY - labelY) < 90 + } + + if let keepAwakeSwitch = switchCandidates.first { + if !isSwitchOn(keepAwakeSwitch) { + keepAwakeSwitch.tap() + sleep(1) + } + return + } + + // Fallback: tap the right side of the Keep Awake row where the switch is rendered. + let appFrame = app.frame + let switchTap = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) + .withOffset(CGVector(dx: appFrame.maxX - 24, dy: labelY)) + switchTap.tap() + sleep(1) + + // Verify we can now observe an "on" switch near this row. + let postTapCandidates = app.switches.allElementsBoundByIndex.filter { element in + abs(element.frame.midY - labelY) < 90 + } + if let keepAwakeSwitch = postTapCandidates.first, !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 }) + } }