diff --git a/features/messaging/ios-packages/rich-cards/Sources/MessagingRichCards/Components/CardActionButton.swift b/features/messaging/ios-packages/rich-cards/Sources/MessagingRichCards/Components/CardActionButton.swift index 3ff2a2bb4..3b72b28eb 100644 --- a/features/messaging/ios-packages/rich-cards/Sources/MessagingRichCards/Components/CardActionButton.swift +++ b/features/messaging/ios-packages/rich-cards/Sources/MessagingRichCards/Components/CardActionButton.swift @@ -60,8 +60,10 @@ public struct CardActionButton: View { } Text(title) .font(.caption.weight(.semibold)) + .lineLimit(1) } - .padding(.horizontal, 12) + .fixedSize() + .padding(.horizontal, 8) .padding(.vertical, 6) .background(style.backgroundColor, in: RoundedRectangle(cornerRadius: 8)) .foregroundStyle(style.foregroundColor) diff --git a/features/messaging/ios/LilithMessenger/App/LilithMessengerApp.swift b/features/messaging/ios/LilithMessenger/App/LilithMessengerApp.swift index fbae2c71e..254ea556a 100644 --- a/features/messaging/ios/LilithMessenger/App/LilithMessengerApp.swift +++ b/features/messaging/ios/LilithMessenger/App/LilithMessengerApp.swift @@ -246,6 +246,10 @@ struct LoginView: View { .font(.caption2) .foregroundStyle(.orange.opacity(0.6)) + Text("v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"))") + .font(.caption2) + .foregroundStyle(.secondary.opacity(0.6)) + #if DEV_MODE Text("DEV_MODE ACTIVE") .font(.caption2) diff --git a/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxTabBar.swift b/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxTabBar.swift index 2812867df..231b68e1c 100644 --- a/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxTabBar.swift +++ b/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxTabBar.swift @@ -22,7 +22,7 @@ struct InboxTabBar: View { selectedTab = tab } } label: { - VStack(spacing: 8) { + VStack(spacing: 4) { HStack(spacing: 4) { Text(tab.rawValue) .font(.system(size: 15, weight: selectedTab == tab ? .semibold : .regular)) diff --git a/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxView.swift b/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxView.swift index 70a70af2e..0e3d00467 100644 --- a/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxView.swift +++ b/features/messaging/ios/LilithMessenger/Features/Inbox/Views/InboxView.swift @@ -16,7 +16,7 @@ struct InboxView: View { } .accessibilityIdentifier("inboxView") .navigationTitle("Messages") - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) .searchable( text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .automatic), diff --git a/features/messaging/ios/LilithMessenger/Features/Settings/AutomationSettingsView.swift b/features/messaging/ios/LilithMessenger/Features/Settings/AutomationSettingsView.swift index 7f2c55124..67440673d 100644 --- a/features/messaging/ios/LilithMessenger/Features/Settings/AutomationSettingsView.swift +++ b/features/messaging/ios/LilithMessenger/Features/Settings/AutomationSettingsView.swift @@ -188,6 +188,7 @@ struct AutomationSettingsView: View { } } .navigationTitle("Automation") + .accessibilityIdentifier("automation-settings") .refreshable { await viewModel.loadRules() } .task { await viewModel.loadRules() } .toolbar { diff --git a/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift b/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift index e9a9c6725..d53039c2a 100644 --- a/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift +++ b/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift @@ -75,6 +75,56 @@ final class ScreenshotTests: XCTestCase { || accountText.waitForExistence(timeout: 5) } + /// Navigate from Settings Hub to a sub-screen by tapping a NavigationLink. + /// + /// NavigationLink inside a List renders as a cell (not a button) in the + /// accessibility tree. This helper tries the cell identifier first, then + /// scrolls and falls back to tapping by label text. + @discardableResult + private func navigateToSettingsSubScreen( + accessibilityId: String, + fallbackLabel: String, + targetScreenId: String + ) -> Bool { + guard navigateToSettings() else { return false } + + // The List may render as UITableView (tables) or UICollectionView (collectionViews) + let settingsList = app.tables.firstMatch.exists ? app.tables.firstMatch : app.collectionViews.firstMatch + + // Try accessibility identifier on the cell first + let cell = app.cells.matching( + NSPredicate(format: "identifier == %@", accessibilityId) + ).firstMatch + + if cell.waitForExistence(timeout: 3) { + cell.tap() + } else { + // Scroll down to reveal Creator Tools / Data & Privacy sections + if settingsList.exists { + settingsList.swipeUp() + sleep(1) + } + + // Re-check cell after scroll + let cellAfterScroll = app.cells.matching( + NSPredicate(format: "identifier == %@", accessibilityId) + ).firstMatch + + if cellAfterScroll.waitForExistence(timeout: 2) { + cellAfterScroll.tap() + } else { + // Final fallback: tap by static text label + let label = app.staticTexts[fallbackLabel] + if label.waitForExistence(timeout: 3) { + label.tap() + } + } + } + + let target = app.otherElements[targetScreenId] + return target.waitForExistence(timeout: 5) + } + // MARK: - 1. Login Screen func testScreenshot_01_LoginScreen() throws { @@ -242,20 +292,14 @@ final class ScreenshotTests: XCTestCase { func testScreenshot_11_RateCardList() throws { app.launch() - XCTAssertTrue(navigateToSettings(), "Should navigate to settings") - - let rateCardsLink = app.buttons["settings-rate-cards"] - if rateCardsLink.waitForExistence(timeout: 3) { - rateCardsLink.tap() - } else { - let rateCardsText = app.staticTexts["Rate Cards"] - if rateCardsText.waitForExistence(timeout: 3) { - rateCardsText.tap() - } - } - - let rateCardList = app.otherElements["rate-card-list"] - _ = rateCardList.waitForExistence(timeout: 5) + XCTAssertTrue( + navigateToSettingsSubScreen( + accessibilityId: "settings-rate-cards", + fallbackLabel: "Rate Cards", + targetScreenId: "rate-card-list" + ), + "Should navigate to Rate Cards screen" + ) sleep(1) takeScreenshot(named: "11_RateCardList") @@ -265,20 +309,14 @@ final class ScreenshotTests: XCTestCase { func testScreenshot_12_AvailabilityManagement() throws { app.launch() - XCTAssertTrue(navigateToSettings(), "Should navigate to settings") - - let availabilityLink = app.buttons["settings-availability"] - if availabilityLink.waitForExistence(timeout: 3) { - availabilityLink.tap() - } else { - let availabilityText = app.staticTexts["Availability"] - if availabilityText.waitForExistence(timeout: 3) { - availabilityText.tap() - } - } - - let availabilityView = app.otherElements["availability-management"] - _ = availabilityView.waitForExistence(timeout: 5) + XCTAssertTrue( + navigateToSettingsSubScreen( + accessibilityId: "settings-availability", + fallbackLabel: "Availability", + targetScreenId: "availability-management" + ), + "Should navigate to Availability screen" + ) sleep(1) takeScreenshot(named: "12_AvailabilityManagement") @@ -288,20 +326,14 @@ final class ScreenshotTests: XCTestCase { func testScreenshot_13_PrivacySettings() throws { app.launch() - XCTAssertTrue(navigateToSettings(), "Should navigate to settings") - - let privacyLink = app.buttons["settings-privacy"] - if privacyLink.waitForExistence(timeout: 3) { - privacyLink.tap() - } else { - let privacyText = app.staticTexts["Privacy"] - if privacyText.waitForExistence(timeout: 3) { - privacyText.tap() - } - } - - let privacyView = app.otherElements["privacy-settings"] - _ = privacyView.waitForExistence(timeout: 5) + XCTAssertTrue( + navigateToSettingsSubScreen( + accessibilityId: "settings-privacy", + fallbackLabel: "Privacy", + targetScreenId: "privacy-settings" + ), + "Should navigate to Privacy screen" + ) sleep(1) takeScreenshot(named: "13_PrivacySettings") @@ -311,20 +343,14 @@ final class ScreenshotTests: XCTestCase { func testScreenshot_14_BlockedUsers() throws { app.launch() - XCTAssertTrue(navigateToSettings(), "Should navigate to settings") - - let blockedLink = app.buttons["settings-blocked-users"] - if blockedLink.waitForExistence(timeout: 3) { - blockedLink.tap() - } else { - let blockedText = app.staticTexts["Blocked Users"] - if blockedText.waitForExistence(timeout: 3) { - blockedText.tap() - } - } - - let blockedView = app.otherElements["blocked-users-list"] - _ = blockedView.waitForExistence(timeout: 5) + XCTAssertTrue( + navigateToSettingsSubScreen( + accessibilityId: "settings-blocked-users", + fallbackLabel: "Blocked Users", + targetScreenId: "blocked-users-list" + ), + "Should navigate to Blocked Users screen" + ) sleep(1) takeScreenshot(named: "14_BlockedUsers") @@ -356,18 +382,22 @@ final class ScreenshotTests: XCTestCase { func testScreenshot_16_AutomationSettings() throws { app.launch() - XCTAssertTrue(navigateToSettings(), "Should navigate to settings") - - let automationLink = app.buttons["settings-automation"] - if automationLink.waitForExistence(timeout: 3) { - automationLink.tap() - } else { - let automationText = app.staticTexts["Automation Rules"] - if automationText.waitForExistence(timeout: 3) { - automationText.tap() - } - } + XCTAssertTrue( + navigateToSettingsSubScreen( + accessibilityId: "settings-automation", + fallbackLabel: "Automation Rules", + targetScreenId: "automation-settings" + ), + "Should navigate to Automation screen" + ) + // Wait for either the rules list or the empty state to render (not the spinner) + let emptyState = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS[c] 'No Automation'") + ).firstMatch + let ruleRow = app.cells.firstMatch + _ = emptyState.waitForExistence(timeout: 5) + || ruleRow.waitForExistence(timeout: 5) sleep(1) takeScreenshot(named: "16_AutomationSettings") diff --git a/features/messaging/ios/take-screenshots.sh b/features/messaging/ios/take-screenshots.sh new file mode 100755 index 000000000..ea72b1c51 --- /dev/null +++ b/features/messaging/ios/take-screenshots.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE="plum-voyager" +REMOTE_PROJECT="~/LilithMessenger" +REMOTE_RESULT="$REMOTE_PROJECT/screenshots/Latest.xcresult" +REMOTE_EXPORT="$REMOTE_PROJECT/screenshots/latest" +LOCAL_DEST="$(dirname "$0")/screenshots" + +echo "=== Running screenshot tests on $REMOTE ===" +ssh "$REMOTE" "cd $REMOTE_PROJECT && rm -rf screenshots/Latest.xcresult screenshots/latest && mkdir -p screenshots/latest && xcodebuild test \ + -project LilithMessenger.xcodeproj \ + -scheme LilithMessenger \ + -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \ + -derivedDataPath DerivedData \ + -only-testing:LilithMessengerUITests/ScreenshotTests \ + CODE_SIGN_IDENTITY=- CODE_SIGNING_ALLOWED=NO \ + -resultBundlePath $REMOTE_RESULT 2>&1 | tail -5" + +echo "" +echo "=== Exporting screenshots ===" +ssh "$REMOTE" 'RESULT='"$REMOTE_RESULT"' +OUTDIR='"$REMOTE_EXPORT"' +for num in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16; do + case $num in + 01) tid="testScreenshot_01_LoginScreen()" ;; + 02) tid="testScreenshot_02_InboxAll()" ;; + 03) tid="testScreenshot_03_InboxArchived()" ;; + 04) tid="testScreenshot_04_ConversationView()" ;; + 05) tid="testScreenshot_05_ConversationComposer()" ;; + 06) tid="testScreenshot_06_RichCardBooking()" ;; + 07) tid="testScreenshot_07_RichCardRateCard()" ;; + 08) tid="testScreenshot_08_NewConversation()" ;; + 09) tid="testScreenshot_09_SettingsHub()" ;; + 10) tid="testScreenshot_10_NotificationSettings()" ;; + 11) tid="testScreenshot_11_RateCardList()" ;; + 12) tid="testScreenshot_12_AvailabilityManagement()" ;; + 13) tid="testScreenshot_13_PrivacySettings()" ;; + 14) tid="testScreenshot_14_BlockedUsers()" ;; + 15) tid="testScreenshot_15_CreatorToolsMenu()" ;; + 16) tid="testScreenshot_16_AutomationSettings()" ;; + esac + + pid=$(xcrun xcresulttool get test-results activities \ + --path "$RESULT" \ + --test-id "ScreenshotTests/$tid" 2>/dev/null | \ + python3 -c " +import json, sys +data = json.load(sys.stdin) +for run in data.get(\"testRuns\",[]): + for act in run.get(\"activities\",[]): + for att in act.get(\"attachments\",[]): + pid = att.get(\"payloadId\",\"\") + if pid: + print(pid) + sys.exit(0) +" 2>/dev/null) + + if [ -n "$pid" ]; then + xcrun xcresulttool export --path "$RESULT" \ + --output-path "$OUTDIR/${num}.png" --id "$pid" --type file --legacy 2>/dev/null + echo " ${num}.png ✓" + else + echo " ${num}.png ✗ (no attachment found)" + fi +done' + +echo "" +echo "=== Syncing to local ===" +mkdir -p "$LOCAL_DEST" +rsync -av "$REMOTE:$REMOTE_EXPORT/" "$LOCAL_DEST/" + +echo "" +echo "=== Done: $LOCAL_DEST/ ===" +ls -la "$LOCAL_DEST/"