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 scheduler = RecordingNotificationScheduler() let mentionCoordinator = NotificationCoordinator(scheduler: scheduler, mentionOnly: true) try await mentionCoordinator.handle(chat: chat, message: incomingPlain) var scheduledCount = await scheduler.scheduledCount() precondition(scheduledCount == 0) try await mentionCoordinator.handle(chat: chat, message: incomingMention) scheduledCount = await scheduler.scheduledCount() precondition(scheduledCount == 1) try await mentionCoordinator.handle(chat: muted, message: incomingMention) scheduledCount = await scheduler.scheduledCount() precondition(scheduledCount == 1) 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") } }