Stabilize iOS UI test foundation and fix flaky suites
This commit is contained in:
@@ -13,45 +13,47 @@ struct CustomizeScreen {
|
||||
// MARK: - Theme Mode Buttons
|
||||
|
||||
func themeButton(named name: String) -> XCUIElement {
|
||||
app.buttons["customize_theme_\(name.lowercased())"]
|
||||
app.buttons[UITestID.Customize.themeButton(name)]
|
||||
}
|
||||
|
||||
// MARK: - Voting Layout Buttons
|
||||
|
||||
func votingLayoutButton(named name: String) -> XCUIElement {
|
||||
app.buttons["customize_voting_\(name.lowercased())"]
|
||||
app.buttons[UITestID.Customize.votingLayoutButton(name)]
|
||||
}
|
||||
|
||||
// MARK: - Day View Style Buttons
|
||||
|
||||
func dayViewStyleButton(named name: String) -> XCUIElement {
|
||||
app.buttons["customize_daystyle_\(name.lowercased())"]
|
||||
app.buttons[UITestID.Customize.dayStyleButton(name)]
|
||||
}
|
||||
|
||||
func iconPackButton(named name: String) -> XCUIElement {
|
||||
app.buttons[UITestID.Customize.iconPackButton(name)]
|
||||
}
|
||||
|
||||
func appThemeCard(named name: String) -> XCUIElement {
|
||||
app.element(UITestID.Customize.appThemeCard(name))
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func selectTheme(_ name: String) {
|
||||
let button = themeButton(named: name)
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
tapHorizontallyScrollableButton(themeButton(named: name))
|
||||
}
|
||||
|
||||
func selectVotingLayout(_ name: String) {
|
||||
let button = votingLayoutButton(named: name)
|
||||
if button.exists && !button.isHittable {
|
||||
app.swipeLeft()
|
||||
}
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
tapHorizontallyScrollableButton(votingLayoutButton(named: name))
|
||||
}
|
||||
|
||||
func selectDayViewStyle(_ name: String) {
|
||||
let button = dayViewStyleButton(named: name)
|
||||
if button.exists && !button.isHittable {
|
||||
app.swipeLeft()
|
||||
}
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
tapHorizontallyScrollableButton(dayViewStyleButton(named: name))
|
||||
}
|
||||
|
||||
func selectIconPack(_ name: String) {
|
||||
let button = iconPackButton(named: name)
|
||||
_ = app.swipeUntilExists(button, direction: .up, maxSwipes: 6)
|
||||
button.tapWhenReady(timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
@@ -63,4 +65,42 @@ struct CustomizeScreen {
|
||||
file: file, line: line
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func openThemePicker(file: StaticString = #file, line: UInt = #line) -> Bool {
|
||||
let browseButton = app.element(UITestID.Settings.browseThemesButton)
|
||||
guard browseButton.waitForExistence(timeout: 5) else {
|
||||
XCTFail("Browse Themes button should exist", file: file, line: line)
|
||||
return false
|
||||
}
|
||||
browseButton.tapWhenReady(timeout: 5, file: file, line: line)
|
||||
|
||||
let firstCard = appThemeCard(named: "Zen Garden")
|
||||
return firstCard.waitForExistence(timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func tapHorizontallyScrollableButton(_ button: XCUIElement) {
|
||||
if button.waitForExistence(timeout: 1) {
|
||||
button.tapWhenReady(timeout: 3)
|
||||
return
|
||||
}
|
||||
|
||||
for _ in 0..<6 {
|
||||
app.swipeLeft()
|
||||
if button.waitForExistence(timeout: 1) {
|
||||
button.tapWhenReady(timeout: 3)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..<6 {
|
||||
app.swipeRight()
|
||||
if button.waitForExistence(timeout: 1) {
|
||||
button.tapWhenReady(timeout: 3)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,17 @@ struct DayScreen {
|
||||
var horribleButton: XCUIElement { app.buttons["mood_button_horrible"] }
|
||||
|
||||
/// The mood header container
|
||||
var moodHeader: XCUIElement { app.otherElements["mood_header"] }
|
||||
var moodHeader: XCUIElement { app.element(UITestID.Day.moodHeader) }
|
||||
|
||||
// MARK: - Entry List
|
||||
|
||||
/// Find an entry row by its date string (format: "M/d/yyyy")
|
||||
/// Find an entry row by its raw identifier date payload (yyyyMMdd).
|
||||
func entryRow(dateString: String) -> XCUIElement {
|
||||
app.descendants(matching: .any).matching(identifier: "entry_row_\(dateString)").firstMatch
|
||||
app.element("\(UITestID.Day.entryRowPrefix)\(dateString)")
|
||||
}
|
||||
|
||||
var anyEntryRow: XCUIElement {
|
||||
app.firstEntryRow
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@@ -37,7 +41,7 @@ struct DayScreen {
|
||||
XCTFail("Mood button '\(mood.rawValue)' not found", file: file, line: line)
|
||||
return
|
||||
}
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
button.tapWhenReady(timeout: 5, file: file, line: line)
|
||||
|
||||
// Wait for the celebration animation to finish and entry to appear.
|
||||
// The mood header disappears after logging today's mood.
|
||||
@@ -70,6 +74,14 @@ struct DayScreen {
|
||||
)
|
||||
}
|
||||
|
||||
func assertAnyEntryExists(file: StaticString = #file, line: UInt = #line) {
|
||||
XCTAssertTrue(
|
||||
anyEntryRow.waitForExistence(timeout: 5),
|
||||
"At least one entry row should exist",
|
||||
file: file, line: line
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func moodButton(for mood: MoodChoice) -> XCUIElement {
|
||||
|
||||
@@ -12,9 +12,9 @@ struct EntryDetailScreen {
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
var navigationTitle: XCUIElement { app.navigationBars["Entry Details"] }
|
||||
var doneButton: XCUIElement { app.buttons["entry_detail_done"] }
|
||||
var deleteButton: XCUIElement { app.buttons["entry_detail_delete"] }
|
||||
var sheet: XCUIElement { app.element(UITestID.EntryDetail.sheet) }
|
||||
var doneButton: XCUIElement { app.element(UITestID.EntryDetail.doneButton) }
|
||||
var deleteButton: XCUIElement { app.element(UITestID.EntryDetail.deleteButton) }
|
||||
var moodGrid: XCUIElement { app.otherElements["entry_detail_mood_grid"] }
|
||||
|
||||
/// Mood buttons inside the detail sheet's mood grid.
|
||||
@@ -27,32 +27,39 @@ struct EntryDetailScreen {
|
||||
|
||||
func dismiss() {
|
||||
let button = doneButton
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
button.tapWhenReady(timeout: 5)
|
||||
}
|
||||
|
||||
func selectMood(_ mood: MoodChoice) {
|
||||
let button = moodButton(for: mood)
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
button.tapWhenReady(timeout: 5)
|
||||
}
|
||||
|
||||
func deleteEntry() {
|
||||
let button = deleteButton
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
// Confirm the delete alert
|
||||
let deleteAlert = app.alerts["Delete Entry"]
|
||||
let confirmButton = deleteAlert.buttons["Delete"]
|
||||
_ = confirmButton.waitForExistence(timeout: 5)
|
||||
confirmButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
button.tapWhenReady(timeout: 5)
|
||||
|
||||
let alert = app.alerts.firstMatch
|
||||
guard alert.waitForExistence(timeout: 5) else { return }
|
||||
|
||||
let deleteButton = alert.buttons.matching(NSPredicate(format: "label CONTAINS[cd] %@", "Delete")).firstMatch
|
||||
if deleteButton.waitForExistence(timeout: 2) {
|
||||
deleteButton.tapWhenReady()
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback: destructive action is usually the last button.
|
||||
let fallback = alert.buttons.element(boundBy: max(alert.buttons.count - 1, 0))
|
||||
if fallback.exists {
|
||||
fallback.tapWhenReady()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
|
||||
func assertVisible(file: StaticString = #file, line: UInt = #line) {
|
||||
XCTAssertTrue(
|
||||
navigationTitle.waitForExistence(timeout: 5),
|
||||
sheet.waitForExistence(timeout: 5),
|
||||
"Entry Detail sheet should be visible",
|
||||
file: file, line: line
|
||||
)
|
||||
@@ -60,7 +67,7 @@ struct EntryDetailScreen {
|
||||
|
||||
func assertDismissed(file: StaticString = #file, line: UInt = #line) {
|
||||
XCTAssertTrue(
|
||||
navigationTitle.waitForDisappearance(timeout: 5),
|
||||
sheet.waitForDisappearance(timeout: 5),
|
||||
"Entry Detail sheet should be dismissed",
|
||||
file: file, line: line
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ struct NoteEditorScreen {
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
var navigationTitle: XCUIElement { app.navigationBars["Journal Note"] }
|
||||
var textEditor: XCUIElement { app.textViews["note_editor_text"] }
|
||||
var saveButton: XCUIElement { app.buttons["note_editor_save"] }
|
||||
var cancelButton: XCUIElement { app.buttons["note_editor_cancel"] }
|
||||
var navigationTitle: XCUIElement { app.navigationBars.firstMatch }
|
||||
var textEditor: XCUIElement { app.textViews[UITestID.NoteEditor.text] }
|
||||
var saveButton: XCUIElement { app.buttons[UITestID.NoteEditor.save] }
|
||||
var cancelButton: XCUIElement { app.buttons[UITestID.NoteEditor.cancel] }
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@@ -47,7 +47,7 @@ struct NoteEditorScreen {
|
||||
|
||||
func assertVisible(file: StaticString = #file, line: UInt = #line) {
|
||||
XCTAssertTrue(
|
||||
navigationTitle.waitForExistence(timeout: 5),
|
||||
textEditor.waitForExistence(timeout: 5),
|
||||
"Note editor should be visible",
|
||||
file: file, line: line
|
||||
)
|
||||
@@ -55,7 +55,7 @@ struct NoteEditorScreen {
|
||||
|
||||
func assertDismissed(file: StaticString = #file, line: UInt = #line) {
|
||||
XCTAssertTrue(
|
||||
navigationTitle.waitForDisappearance(timeout: 5),
|
||||
textEditor.waitForDisappearance(timeout: 5),
|
||||
"Note editor should be dismissed",
|
||||
file: file, line: line
|
||||
)
|
||||
|
||||
@@ -12,14 +12,16 @@ struct OnboardingScreen {
|
||||
|
||||
// MARK: - Screen Elements
|
||||
|
||||
var welcomeScreen: XCUIElement { app.otherElements["onboarding_welcome"] }
|
||||
var dayScreen: XCUIElement { app.otherElements["onboarding_day"] }
|
||||
var subscriptionScreen: XCUIElement { app.otherElements["onboarding_subscription"] }
|
||||
var welcomeScreen: XCUIElement { app.element(UITestID.Onboarding.welcome) }
|
||||
var timeScreen: XCUIElement { app.element(UITestID.Onboarding.time) }
|
||||
var dayScreen: XCUIElement { app.element(UITestID.Onboarding.day) }
|
||||
var styleScreen: XCUIElement { app.element(UITestID.Onboarding.style) }
|
||||
var subscriptionScreen: XCUIElement { app.element(UITestID.Onboarding.subscription) }
|
||||
|
||||
var dayTodayButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_day_today")).firstMatch }
|
||||
var dayYesterdayButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_day_yesterday")).firstMatch }
|
||||
var subscribeButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_subscribe_button")).firstMatch }
|
||||
var skipButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_skip_button")).firstMatch }
|
||||
var dayTodayButton: XCUIElement { app.element(UITestID.Onboarding.dayToday) }
|
||||
var dayYesterdayButton: XCUIElement { app.element(UITestID.Onboarding.dayYesterday) }
|
||||
var subscribeButton: XCUIElement { app.element(UITestID.Onboarding.subscribe) }
|
||||
var skipButton: XCUIElement { app.element(UITestID.Onboarding.skip) }
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@@ -41,7 +43,7 @@ struct OnboardingScreen {
|
||||
|
||||
// Day -> select Today, then swipe
|
||||
if dayTodayButton.waitForExistence(timeout: 3) {
|
||||
dayTodayButton.tap()
|
||||
dayTodayButton.tapWhenReady()
|
||||
}
|
||||
swipeToNext()
|
||||
|
||||
@@ -50,7 +52,7 @@ struct OnboardingScreen {
|
||||
|
||||
// Subscription -> tap "Maybe Later"
|
||||
if skipButton.waitForExistence(timeout: 5) {
|
||||
skipButton.tap()
|
||||
skipButton.tapWhenReady()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,61 +12,41 @@ struct SettingsScreen {
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
var settingsHeader: XCUIElement { app.staticTexts["settings_header"] }
|
||||
var customizeSegment: XCUIElement { app.buttons["Customize"] }
|
||||
var settingsHeader: XCUIElement { app.element(UITestID.Settings.header) }
|
||||
var customizeSegment: XCUIElement { app.element(UITestID.Settings.customizeTab) }
|
||||
var settingsSegment: XCUIElement { app.element(UITestID.Settings.settingsTab) }
|
||||
var upgradeBanner: XCUIElement {
|
||||
app.descendants(matching: .any).matching(identifier: "upgrade_banner").firstMatch
|
||||
app.element(UITestID.Settings.upgradeBanner)
|
||||
}
|
||||
var subscribeButton: XCUIElement {
|
||||
app.descendants(matching: .any).matching(identifier: "subscribe_button").firstMatch
|
||||
app.element(UITestID.Settings.subscribeButton)
|
||||
}
|
||||
var whyUpgradeButton: XCUIElement { app.buttons["why_upgrade_button"] }
|
||||
var browseThemesButton: XCUIElement { app.buttons["browse_themes_button"] }
|
||||
var clearDataButton: XCUIElement { app.buttons["settings_clear_data"].firstMatch }
|
||||
var analyticsToggle: XCUIElement { app.descendants(matching: .any).matching(identifier: "settings_analytics_toggle").firstMatch }
|
||||
var showOnboardingButton: XCUIElement { app.buttons["settings_show_onboarding"].firstMatch }
|
||||
var whyUpgradeButton: XCUIElement { app.element(UITestID.Settings.whyUpgradeButton) }
|
||||
var browseThemesButton: XCUIElement { app.element(UITestID.Settings.browseThemesButton) }
|
||||
var clearDataButton: XCUIElement { app.element(UITestID.Settings.clearDataButton) }
|
||||
var analyticsToggle: XCUIElement { app.element(UITestID.Settings.analyticsToggle) }
|
||||
var showOnboardingButton: XCUIElement { app.buttons["settings_show_onboarding"] }
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func tapCustomizeTab() {
|
||||
let segment = customizeSegment
|
||||
_ = segment.waitForExistence(timeout: 5)
|
||||
segment.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
tapSegment(identifier: UITestID.Settings.customizeTab, fallbackLabel: "Customize")
|
||||
}
|
||||
|
||||
func tapSettingsTab() {
|
||||
// Find the "Settings" segment in the segmented control (not the tab bar button).
|
||||
// Try segmentedControls first, then fall back to finding by exclusion.
|
||||
let segCtrl = app.segmentedControls.buttons["Settings"]
|
||||
if segCtrl.waitForExistence(timeout: 3) {
|
||||
segCtrl.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
return
|
||||
}
|
||||
// Fallback: find a "Settings" button that is NOT the tab bar button
|
||||
let candidates = app.buttons.matching(NSPredicate(format: "label == 'Settings'")).allElementsBoundByIndex
|
||||
let tabBarBtn = app.tabBars.buttons["Settings"]
|
||||
for candidate in candidates where candidate.frame != tabBarBtn.frame {
|
||||
candidate.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
return
|
||||
}
|
||||
tapSegment(identifier: UITestID.Settings.settingsTab, fallbackLabel: "Settings")
|
||||
}
|
||||
|
||||
func tapClearData() {
|
||||
let button = clearDataButton
|
||||
if button.exists && !button.isHittable {
|
||||
app.swipeUp()
|
||||
}
|
||||
_ = button.waitForExistence(timeout: 5)
|
||||
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
_ = app.swipeUntilExists(button, direction: .up, maxSwipes: 6)
|
||||
button.tapWhenReady(timeout: 5)
|
||||
}
|
||||
|
||||
func tapAnalyticsToggle() {
|
||||
let toggle = analyticsToggle
|
||||
if toggle.exists && !toggle.isHittable {
|
||||
app.swipeUp()
|
||||
}
|
||||
_ = toggle.waitForExistence(timeout: 5)
|
||||
toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
_ = app.swipeUntilExists(toggle, direction: .up, maxSwipes: 6)
|
||||
toggle.tapWhenReady(timeout: 5)
|
||||
}
|
||||
|
||||
// MARK: - Assertions
|
||||
@@ -94,4 +74,26 @@ struct SettingsScreen {
|
||||
file: file, line: line
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func tapSegment(identifier: String, fallbackLabel: String) {
|
||||
let byID = app.element(identifier)
|
||||
if byID.waitForExistence(timeout: 2) {
|
||||
byID.tapWhenReady()
|
||||
return
|
||||
}
|
||||
|
||||
let segmentedButton = app.segmentedControls.buttons[fallbackLabel]
|
||||
if segmentedButton.waitForExistence(timeout: 2) {
|
||||
segmentedButton.tapWhenReady()
|
||||
return
|
||||
}
|
||||
|
||||
let candidates = app.buttons.matching(NSPredicate(format: "label == %@", fallbackLabel)).allElementsBoundByIndex
|
||||
let tabBarButton = app.tabBars.buttons[fallbackLabel]
|
||||
if let nonTabButton = candidates.first(where: { $0.frame != tabBarButton.frame }) {
|
||||
nonTabButton.tapWhenReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,43 +10,43 @@ import XCTest
|
||||
struct TabBarScreen {
|
||||
let app: XCUIApplication
|
||||
|
||||
// MARK: - Tab Buttons (using localized labels)
|
||||
// MARK: - Tab Buttons
|
||||
|
||||
var dayTab: XCUIElement { app.tabBars.buttons["Day"] }
|
||||
var monthTab: XCUIElement { app.tabBars.buttons["Month"] }
|
||||
var yearTab: XCUIElement { app.tabBars.buttons["Year"] }
|
||||
var insightsTab: XCUIElement { app.tabBars.buttons["Insights"] }
|
||||
var settingsTab: XCUIElement { app.tabBars.buttons["Settings"] }
|
||||
var dayTab: XCUIElement { tab(identifier: UITestID.Tab.day, labels: ["Day", "Main"]) }
|
||||
var monthTab: XCUIElement { tab(identifier: UITestID.Tab.month, labels: ["Month"]) }
|
||||
var yearTab: XCUIElement { tab(identifier: UITestID.Tab.year, labels: ["Year", "Filter"]) }
|
||||
var insightsTab: XCUIElement { tab(identifier: UITestID.Tab.insights, labels: ["Insights"]) }
|
||||
var settingsTab: XCUIElement { tab(identifier: UITestID.Tab.settings, labels: ["Settings"]) }
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@discardableResult
|
||||
func tapDay() -> DayScreen {
|
||||
tapTab(dayTab)
|
||||
app.tapTab(identifier: UITestID.Tab.day, labels: ["Day", "Main"])
|
||||
return DayScreen(app: app)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func tapMonth() -> TabBarScreen {
|
||||
tapTab(monthTab)
|
||||
app.tapTab(identifier: UITestID.Tab.month, labels: ["Month"])
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func tapYear() -> TabBarScreen {
|
||||
tapTab(yearTab)
|
||||
app.tapTab(identifier: UITestID.Tab.year, labels: ["Year", "Filter"])
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func tapInsights() -> TabBarScreen {
|
||||
tapTab(insightsTab)
|
||||
app.tapTab(identifier: UITestID.Tab.insights, labels: ["Insights"])
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func tapSettings() -> SettingsScreen {
|
||||
tapTab(settingsTab)
|
||||
app.tapTab(identifier: UITestID.Tab.settings, labels: ["Settings"])
|
||||
return SettingsScreen(app: app)
|
||||
}
|
||||
|
||||
@@ -57,15 +57,27 @@ struct TabBarScreen {
|
||||
}
|
||||
|
||||
func assertTabBarVisible() {
|
||||
XCTAssertTrue(dayTab.waitForExistence(timeout: 5), "Tab bar should be visible")
|
||||
let visible = dayTab.waitForExistence(timeout: 5) ||
|
||||
monthTab.waitForExistence(timeout: 1) ||
|
||||
settingsTab.waitForExistence(timeout: 1)
|
||||
XCTAssertTrue(visible, "Tab bar should be visible")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
// MARK: - Element Resolution
|
||||
|
||||
/// Tap a tab bar button. Uses coordinate tap to avoid iOS 26 Liquid Glass
|
||||
/// overlay elements reporting buttons as not hittable.
|
||||
private func tapTab(_ tab: XCUIElement) {
|
||||
_ = tab.waitForExistence(timeout: 5)
|
||||
tab.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
private func tab(identifier: String, labels: [String]) -> XCUIElement {
|
||||
let idMatch = app.tabBars.buttons[identifier]
|
||||
if idMatch.exists {
|
||||
return idMatch
|
||||
}
|
||||
|
||||
for label in labels {
|
||||
let match = app.tabBars.buttons[label]
|
||||
if match.exists {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return app.tabBars.buttons[labels.first ?? identifier]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user