feat(messaging): ✨ Add automated message actions with interactive rich card buttons, inbox UI updates for automations, and configuration settings
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
b23495cd9d
commit
dcca1f9b8e
7 changed files with 182 additions and 70 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct InboxView: View {
|
|||
}
|
||||
.accessibilityIdentifier("inboxView")
|
||||
.navigationTitle("Messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.searchable(
|
||||
text: $viewModel.searchQuery,
|
||||
placement: .navigationBarDrawer(displayMode: .automatic),
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ struct AutomationSettingsView: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle("Automation")
|
||||
.accessibilityIdentifier("automation-settings")
|
||||
.refreshable { await viewModel.loadRules() }
|
||||
.task { await viewModel.loadRules() }
|
||||
.toolbar {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
75
features/messaging/ios/take-screenshots.sh
Executable file
75
features/messaging/ios/take-screenshots.sh
Executable file
|
|
@ -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/"
|
||||
Loading…
Add table
Reference in a new issue