242 lines
11 KiB
Swift
242 lines
11 KiB
Swift
import Foundation
|
|
import TeleTuiIOSCore
|
|
|
|
@main
|
|
struct TeleTuiIOSSmokeTests {
|
|
static func main() async throws {
|
|
try await authFlowMatchesAllInteractiveStates()
|
|
try await chatListLoadsDeterministicFakeDataAndFilters()
|
|
try await chatDetailLoadsAndSendsMessage()
|
|
try await messageActionsCoverEditReplyForwardReactDeleteSearchAndCopy()
|
|
try await sessionBridgeFactoryUsesAvailableDefaultBridge()
|
|
try await platformServicesCoverNotificationsMediaVoiceClipboardAndAccounts()
|
|
lifecycleCoordinatorDropsStaleAccountEvents()
|
|
try await profileLoadsFromSelectedChat()
|
|
appStorageUsesApplicationSupportStyleAccountPaths()
|
|
print("TeleTuiIOS smoke tests passed")
|
|
}
|
|
|
|
@MainActor
|
|
private static func authFlowMatchesAllInteractiveStates() async throws {
|
|
let account = Account(id: "fake", displayName: "Fake", databasePath: URL(fileURLWithPath: "/tmp/fake"))
|
|
let store = SessionStore(account: account, bridge: FakeSessionBridge())
|
|
let viewModel = AuthViewModel(store: store)
|
|
|
|
await store.refreshAuthState()
|
|
precondition(store.authState == .waitPhoneNumber)
|
|
await store.refreshNetworkState()
|
|
precondition(store.networkState == .ready)
|
|
store.apply(events: [.typingChanged(.typing(chatId: 1, userId: 10, text: "typing"))])
|
|
precondition(store.typingState == .typing(chatId: 1, userId: 10, text: "typing"))
|
|
|
|
viewModel.phone = "+10000000000"
|
|
await viewModel.submitCurrentStep()
|
|
precondition(store.authState == .waitCode)
|
|
|
|
viewModel.code = "12345"
|
|
await viewModel.submitCurrentStep()
|
|
precondition(store.authState == .waitPassword)
|
|
|
|
viewModel.password = "secret"
|
|
await viewModel.submitCurrentStep()
|
|
precondition(store.authState == .ready)
|
|
}
|
|
|
|
@MainActor
|
|
private static func chatListLoadsDeterministicFakeDataAndFilters() async throws {
|
|
let bridge = FakeSessionBridge(auth: .ready)
|
|
let viewModel = ChatListViewModel(bridge: bridge)
|
|
|
|
await viewModel.load()
|
|
precondition(viewModel.folders.map(\.name) == ["All", "Work"])
|
|
precondition(viewModel.chats.map(\.title) == ["Saved Messages", "iOS Team"])
|
|
|
|
viewModel.searchText = "team"
|
|
precondition(viewModel.filteredChats.map(\.title) == ["iOS Team"])
|
|
|
|
viewModel.searchText = ""
|
|
viewModel.selectedFolderId = 2
|
|
await viewModel.load()
|
|
precondition(viewModel.chats.map(\.title) == ["iOS Team"])
|
|
}
|
|
|
|
@MainActor
|
|
private static func chatDetailLoadsAndSendsMessage() async throws {
|
|
let bridge = FakeSessionBridge(auth: .ready)
|
|
let chat = try await bridge.loadChats(folderId: nil)[0]
|
|
let viewModel = ChatViewModel(chat: chat, bridge: bridge)
|
|
|
|
await viewModel.load()
|
|
precondition(viewModel.messages.count == 1)
|
|
precondition(viewModel.messages[0].date == 1_700_000_000)
|
|
precondition(viewModel.pinnedMessages.map(\.id) == [1])
|
|
if case let .photo(photo) = viewModel.messages[0].media {
|
|
precondition(photo.fileId == 100)
|
|
precondition(photo.width == 1280)
|
|
precondition(photo.height == 720)
|
|
} else {
|
|
preconditionFailure("fake saved message should contain photo media")
|
|
}
|
|
|
|
viewModel.composeText = "Hi from SwiftUI"
|
|
await viewModel.send()
|
|
precondition(viewModel.messages.last?.text == "Hi from SwiftUI")
|
|
precondition(viewModel.composeText.isEmpty)
|
|
}
|
|
|
|
@MainActor
|
|
private static func messageActionsCoverEditReplyForwardReactDeleteSearchAndCopy() async throws {
|
|
let bridge = FakeSessionBridge(auth: .ready)
|
|
let chat = try await bridge.loadChats(folderId: nil)[0]
|
|
let viewModel = ChatViewModel(chat: chat, bridge: bridge)
|
|
|
|
await viewModel.load()
|
|
guard let first = viewModel.messages.first else {
|
|
preconditionFailure("fake chat should contain a message")
|
|
}
|
|
|
|
await viewModel.edit(message: first, text: "Edited text")
|
|
precondition(viewModel.messages.first?.text == "Edited text")
|
|
precondition(viewModel.messages.first?.editDate != nil)
|
|
|
|
viewModel.beginReply(to: viewModel.messages[0])
|
|
viewModel.composeText = "Reply text"
|
|
await viewModel.send()
|
|
precondition(viewModel.messages.last?.replyText == "Reply to #1")
|
|
|
|
await viewModel.react(message: viewModel.messages[0], reaction: "👍")
|
|
precondition(viewModel.messages[0].reactions.first?.emoji == "👍")
|
|
|
|
viewModel.searchText = "reply"
|
|
await viewModel.search()
|
|
precondition(viewModel.searchResults.count == 1)
|
|
|
|
await viewModel.copyPayload(for: viewModel.messages[0])
|
|
precondition(viewModel.copiedPayload == "Edited text")
|
|
|
|
viewModel.composeText = "Draft text"
|
|
await viewModel.saveDraft()
|
|
let draftEvents = try await bridge.pollEvents()
|
|
precondition(draftEvents.contains { event in
|
|
if case let .draftChanged(draft) = event {
|
|
return draft.chatId == chat.id && draft.text == "Draft text"
|
|
}
|
|
return false
|
|
})
|
|
|
|
let photo = try await bridge.downloadPhoto(fileId: 100)
|
|
let voice = try await bridge.downloadVoice(fileId: 200)
|
|
precondition(photo.path == "/tmp/fake-photo-100.jpg")
|
|
precondition(voice.path == "/tmp/fake-voice-200.ogg")
|
|
|
|
await viewModel.forward(message: viewModel.messages[0], to: 2)
|
|
let forwarded = try await bridge.loadHistory(chatId: 2)
|
|
precondition(forwarded.contains { $0.forwardSenderName == "Alice" && $0.text == "Edited text" })
|
|
|
|
await viewModel.delete(message: viewModel.messages[0])
|
|
precondition(!viewModel.messages.contains { $0.id == 1 })
|
|
}
|
|
|
|
@MainActor
|
|
private static func sessionBridgeFactoryUsesAvailableDefaultBridge() async throws {
|
|
let account = Account(id: "factory", displayName: "Factory", databasePath: URL(fileURLWithPath: "/tmp/factory"))
|
|
let bridge = SessionBridgeFactory.makeDefaultBridge(account: account)
|
|
let auth = try await bridge.authState()
|
|
precondition(auth == .ready)
|
|
}
|
|
|
|
@MainActor
|
|
private static func platformServicesCoverNotificationsMediaVoiceClipboardAndAccounts() async throws {
|
|
let root = URL(fileURLWithPath: "/tmp/TeleTuiIOS")
|
|
let paths = AppStoragePaths(root: root)
|
|
let cache = MediaCache(root: paths.mediaCachePath(for: "work"))
|
|
precondition(cache.photoPath(fileId: 10).path == "/tmp/TeleTuiIOS/Accounts/work/Media/photos/10.jpg")
|
|
precondition(cache.voicePath(fileId: 20).path == "/tmp/TeleTuiIOS/Accounts/work/Media/voices/20.ogg")
|
|
|
|
let policy = NotificationPolicy()
|
|
let chat = ChatSummary(id: 1, title: "Chat", lastMessage: "hello", isMuted: false)
|
|
let muted = ChatSummary(id: 2, title: "Muted", lastMessage: "hello", isMuted: true)
|
|
let incomingMention = Message(id: 1, chatId: 1, senderName: "Alice", text: "@me hello", isOutgoing: false)
|
|
let incomingPlain = Message(id: 2, chatId: 1, senderName: "Alice", text: "hello", isOutgoing: false)
|
|
precondition(policy.shouldNotify(chat: chat, message: incomingMention, mentionOnly: true))
|
|
precondition(!policy.shouldNotify(chat: chat, message: incomingPlain, mentionOnly: true))
|
|
precondition(!policy.shouldNotify(chat: muted, message: incomingMention, mentionOnly: false))
|
|
|
|
let clipboard = InMemoryClipboardWriter()
|
|
await clipboard.write(text: "copied")
|
|
let copiedText = await clipboard.currentText()
|
|
precondition(copiedText == "copied")
|
|
|
|
let player = RecordingVoicePlayer()
|
|
let mediaViewModel = MediaViewModel(cache: cache, voicePlayer: player)
|
|
mediaViewModel.showPhoto(path: "/tmp/photo.jpg")
|
|
mediaViewModel.showVoice(path: "/tmp/voice.ogg")
|
|
precondition(mediaViewModel.activePhotoPath == "/tmp/photo.jpg")
|
|
precondition(mediaViewModel.activeVoicePath == "/tmp/voice.ogg")
|
|
let voiceURL = cache.voicePath(fileId: 20)
|
|
await mediaViewModel.playVoice(url: voiceURL)
|
|
precondition(mediaViewModel.isVoicePlaying)
|
|
let loadedURL = await player.currentLoadedURL()
|
|
precondition(loadedURL == voiceURL)
|
|
await mediaViewModel.pauseVoice()
|
|
precondition(!mediaViewModel.isVoicePlaying)
|
|
|
|
let personal = Account(id: "personal", displayName: "Personal", databasePath: paths.databasePath(for: "personal"))
|
|
let work = Account(id: "work", displayName: "Work", databasePath: paths.databasePath(for: "work"))
|
|
let switcher = AccountSwitcherViewModel(accounts: [personal, work], activeAccount: personal)
|
|
switcher.switchToAccount(id: "work")
|
|
precondition(switcher.activeAccount.id == "work")
|
|
precondition(switcher.activeAccount.databasePath.path.hasSuffix("/Accounts/work/tdlib"))
|
|
}
|
|
|
|
@MainActor
|
|
private static func lifecycleCoordinatorDropsStaleAccountEvents() {
|
|
let coordinator = SessionLifecycleCoordinator(activeAccountId: "personal")
|
|
precondition(coordinator.shouldPollEvents)
|
|
|
|
coordinator.enterBackground()
|
|
precondition(!coordinator.shouldPollEvents)
|
|
|
|
coordinator.enterForeground()
|
|
precondition(coordinator.shouldPollEvents)
|
|
|
|
let oldGenerationEvent = ScopedSessionEvent(
|
|
accountId: "personal",
|
|
generation: coordinator.generation,
|
|
event: .authChanged(.ready)
|
|
)
|
|
precondition(coordinator.accepts(oldGenerationEvent))
|
|
|
|
coordinator.switchAccount(to: "work")
|
|
precondition(!coordinator.accepts(oldGenerationEvent))
|
|
|
|
let newGenerationEvent = ScopedSessionEvent(
|
|
accountId: "work",
|
|
generation: coordinator.generation,
|
|
event: .authChanged(.ready)
|
|
)
|
|
precondition(coordinator.accepts(newGenerationEvent))
|
|
}
|
|
|
|
@MainActor
|
|
private static func profileLoadsFromSelectedChat() async throws {
|
|
let bridge = FakeSessionBridge(auth: .ready)
|
|
let viewModel = ProfileViewModel(bridge: bridge)
|
|
|
|
await viewModel.load(chatId: 1)
|
|
precondition(viewModel.profile?.title == "Saved Messages")
|
|
precondition(viewModel.profile?.username == "saved")
|
|
|
|
await viewModel.leave(chatId: 1)
|
|
let chats = try await bridge.loadChats(folderId: nil)
|
|
precondition(!chats.contains { $0.id == 1 })
|
|
}
|
|
|
|
private static func appStorageUsesApplicationSupportStyleAccountPaths() {
|
|
let root = URL(fileURLWithPath: "/tmp/TeleTuiIOS")
|
|
let paths = AppStoragePaths(root: root)
|
|
|
|
precondition(paths.databasePath(for: "work").path == "/tmp/TeleTuiIOS/Accounts/work/tdlib")
|
|
}
|
|
}
|