updated tests
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
7b3a8903a8
commit
86e4382cc2
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user