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 {
|
final class TheNoiseClockUITests: XCTestCase {
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
// 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
|
@MainActor
|
||||||
func testExample() throws {
|
func testAppStoreScreenshots_iPhone69() throws {
|
||||||
// UI tests must launch the application that they test.
|
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
|
app.launchArguments += [
|
||||||
|
"-onboarding.TheNoiseClock.hasCompletedWelcome", "YES",
|
||||||
|
"-onboarding.TheNoiseClock.hasLaunched", "YES"
|
||||||
|
]
|
||||||
app.launch()
|
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
|
@MainActor
|
||||||
func testLaunchPerformance() throws {
|
private func captureAlarmsViewPortraitAndLandscape(_ app: XCUIApplication) {
|
||||||
// This measures how long it takes to launch your application.
|
XCUIDevice.shared.orientation = .portrait
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
sleep(1)
|
||||||
XCUIApplication().launch()
|
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