// // ChatViewModelTests.swift // bitchatTests // // Tests for ChatViewModel using MockTransport for isolation. // This is free and unencumbered software released into the public domain. // import Testing import Foundation @testable import bitchat // MARK: - Test Helpers /// Creates a ChatViewModel with mock dependencies for testing @MainActor private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) { let keychain = MockKeychain() let keychainHelper = MockKeychainHelper() let idBridge = NostrIdentityBridge(keychain: keychainHelper) let identityManager = MockIdentityManager(keychain) let transport = MockTransport() let viewModel = ChatViewModel( keychain: keychain, idBridge: idBridge, identityManager: identityManager, transport: transport ) return (viewModel, transport) } // MARK: - Initialization Tests struct ChatViewModelInitializationTests { @Test @MainActor func initialization_setsDelegate() async { let (viewModel, transport) = makeTestableViewModel() // The viewModel should set itself as the transport delegate #expect(transport.delegate === viewModel) } @Test @MainActor func initialization_startsServices() async { let (_, transport) = makeTestableViewModel() // Services should be started during init #expect(transport.startServicesCallCount == 1) } @Test @MainActor func initialization_hasEmptyMessageList() async { let (viewModel, _) = makeTestableViewModel() // Initial messages may include system messages, but should be limited #expect(viewModel.messages.count < 10) } @Test @MainActor func initialization_setsNickname() async { let (_, transport) = makeTestableViewModel() // Nickname should be set during init #expect(!transport.myNickname.isEmpty) } } // MARK: - Message Sending Tests struct ChatViewModelSendingTests { @Test @MainActor func sendMessage_delegatesToTransport() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("Hello World") #expect(transport.sentMessages.count == 1) #expect(transport.sentMessages.first?.content == "Hello World") } @Test @MainActor func sendMessage_emptyContent_ignored() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("") viewModel.sendMessage(" ") viewModel.sendMessage("\n\t") #expect(transport.sentMessages.isEmpty) } @Test @MainActor func sendMessage_withMentions_sendsContent() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("Hello @alice") #expect(transport.sentMessages.count == 1) #expect(transport.sentMessages.first?.content == "Hello @alice") } @Test @MainActor func sendMessage_command_notSentToTransport() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("/help") // Commands are processed locally, not sent to transport #expect(transport.sentMessages.isEmpty) } } // MARK: - Command Handling Tests struct ChatViewModelCommandTests { @Test @MainActor func sendMessage_commandsNotSentToTransport() async { let (viewModel, transport) = makeTestableViewModel() let commands = ["/nick bob", "/who", "/help", "/clear"] for command in commands { transport.resetRecordings() viewModel.sendMessage(command) try? await Task.sleep(nanoseconds: 100_000_000) #expect(transport.sentMessages.isEmpty) #expect(transport.sentPrivateMessages.isEmpty) } } } // MARK: - Timeline Cap Tests struct ChatViewModelTimelineCapTests { @Test @MainActor func sendMessage_trimsTimelineToCap() async { let (viewModel, _) = makeTestableViewModel() let total = TransportConfig.meshTimelineCap + 5 for i in 0..