Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import XCTest
|
||||
@testable import SocialIO
|
||||
|
||||
final class AppViewModelTests: XCTestCase {
|
||||
@MainActor
|
||||
func testFilteredThreadsRespectMailboxUnreadAndSearch() async throws {
|
||||
let inboxUnread = makeThread(
|
||||
routeID: "inbox-unread",
|
||||
mailbox: .inbox,
|
||||
subject: "Roadmap review",
|
||||
body: "Please review the roadmap before launch.",
|
||||
isUnread: true,
|
||||
isStarred: false,
|
||||
sentAt: .now
|
||||
)
|
||||
let inboxRead = makeThread(
|
||||
routeID: "inbox-read",
|
||||
mailbox: .inbox,
|
||||
subject: "Budget sync",
|
||||
body: "Closing the budget sync loop.",
|
||||
isUnread: false,
|
||||
isStarred: false,
|
||||
sentAt: .now.addingTimeInterval(-60)
|
||||
)
|
||||
let archivedStarred = makeThread(
|
||||
routeID: "archived-starred",
|
||||
mailbox: .archive,
|
||||
subject: "Archived roadmap notes",
|
||||
body: "Keeping the roadmap context around.",
|
||||
isUnread: true,
|
||||
isStarred: true,
|
||||
sentAt: .now.addingTimeInterval(-120)
|
||||
)
|
||||
let model = AppViewModel(
|
||||
service: StubMailService(threadsToLoad: [inboxRead, archivedStarred, inboxUnread]),
|
||||
controlService: StubControlService()
|
||||
)
|
||||
|
||||
await model.load()
|
||||
XCTAssertEqual(model.filteredThreads.map(\.routeID), ["inbox-unread", "inbox-read"])
|
||||
|
||||
model.setUnreadOnly(true)
|
||||
XCTAssertEqual(model.filteredThreads.map(\.routeID), ["inbox-unread"])
|
||||
|
||||
model.selectMailbox(.starred)
|
||||
XCTAssertEqual(model.filteredThreads.map(\.routeID), ["archived-starred"])
|
||||
|
||||
model.setSearchText("context")
|
||||
XCTAssertEqual(model.filteredThreads.map(\.routeID), ["archived-starred"])
|
||||
|
||||
model.setSearchText("launch")
|
||||
XCTAssertTrue(model.filteredThreads.isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testPendingThreadCommandAppliesAfterLoad() async throws {
|
||||
let thread = makeThread(
|
||||
routeID: "launch-copy",
|
||||
mailbox: .inbox,
|
||||
subject: "Launch copy",
|
||||
body: "The second pass is ready.",
|
||||
isUnread: true,
|
||||
isStarred: false,
|
||||
sentAt: .now,
|
||||
messageRouteID: "launch-copy-2"
|
||||
)
|
||||
let model = AppViewModel(
|
||||
service: StubMailService(threadsToLoad: [thread]),
|
||||
controlService: StubControlService()
|
||||
)
|
||||
|
||||
model.apply(
|
||||
command: .thread(
|
||||
threadRouteID: "launch-copy",
|
||||
mailbox: .inbox,
|
||||
messageRouteID: "launch-copy-2",
|
||||
search: nil,
|
||||
unreadOnly: nil
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertNil(model.selectedThread)
|
||||
|
||||
await model.load()
|
||||
|
||||
XCTAssertEqual(model.selectedThread?.routeID, "launch-copy")
|
||||
XCTAssertEqual(model.focusedMessageRouteID, "launch-copy-2")
|
||||
XCTAssertEqual(model.selectedMailbox, .inbox)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testSendCurrentDraftSuccessClosesComposeAndSelectsSentThread() async throws {
|
||||
let sentThread = makeThread(
|
||||
routeID: "sent-1",
|
||||
mailbox: .sent,
|
||||
subject: "Status",
|
||||
body: "Sent body",
|
||||
isUnread: false,
|
||||
isStarred: false,
|
||||
sentAt: .now
|
||||
)
|
||||
let service = StubMailService(threadsToLoad: [], sendResult: .success(sentThread))
|
||||
let model = AppViewModel(service: service, controlService: StubControlService())
|
||||
let draft = ComposeDraft(to: "team@social.io", subject: "Status", body: "Sent body")
|
||||
model.composeDraft = draft
|
||||
model.isComposing = true
|
||||
|
||||
let didSend = await model.sendCurrentDraft()
|
||||
|
||||
XCTAssertTrue(didSend)
|
||||
XCTAssertEqual(service.sentDrafts, [draft])
|
||||
XCTAssertFalse(model.isComposing)
|
||||
XCTAssertFalse(model.isSending)
|
||||
XCTAssertEqual(model.selectedMailbox, .sent)
|
||||
XCTAssertEqual(model.selectedThread?.routeID, "sent-1")
|
||||
XCTAssertEqual(model.threads.first?.routeID, "sent-1")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testSendCurrentDraftFailureKeepsComposeOpenAndPreservesDraft() async throws {
|
||||
let service = StubMailService(threadsToLoad: [], sendResult: .failure(TestError.sendFailed))
|
||||
let model = AppViewModel(service: service, controlService: StubControlService())
|
||||
let draft = ComposeDraft(to: "team@social.io", subject: "Status", body: "Body")
|
||||
model.composeDraft = draft
|
||||
model.isComposing = true
|
||||
|
||||
let didSend = await model.sendCurrentDraft()
|
||||
|
||||
XCTAssertFalse(didSend)
|
||||
XCTAssertEqual(service.sentDrafts, [draft])
|
||||
XCTAssertTrue(model.isComposing)
|
||||
XCTAssertFalse(model.isSending)
|
||||
XCTAssertEqual(model.composeDraft, draft)
|
||||
XCTAssertEqual(model.errorMessage, "Unable to send message.")
|
||||
XCTAssertTrue(model.threads.isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testBeginBackendControlCanRestartAfterPreviousStreamFinishes() async throws {
|
||||
let model = AppViewModel(
|
||||
service: StubMailService(threadsToLoad: [
|
||||
makeThread(
|
||||
routeID: "launch-copy",
|
||||
mailbox: .inbox,
|
||||
subject: "Launch copy",
|
||||
body: "Mail body",
|
||||
isUnread: true,
|
||||
isStarred: false,
|
||||
sentAt: .now
|
||||
)
|
||||
]),
|
||||
controlService: StubControlService(commandsPerCall: [
|
||||
[.mailbox(mailbox: .archive, search: nil, unreadOnly: true)],
|
||||
[.mailbox(mailbox: .starred, search: "roadmap", unreadOnly: false)]
|
||||
])
|
||||
)
|
||||
|
||||
await model.load()
|
||||
await model.beginBackendControl()
|
||||
|
||||
XCTAssertEqual(model.selectedMailbox, .archive)
|
||||
XCTAssertTrue(model.showUnreadOnly)
|
||||
|
||||
await model.beginBackendControl()
|
||||
|
||||
XCTAssertEqual(model.selectedMailbox, .starred)
|
||||
XCTAssertEqual(model.searchText, "roadmap")
|
||||
XCTAssertFalse(model.showUnreadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
private enum TestError: Error {
|
||||
case sendFailed
|
||||
}
|
||||
|
||||
private final class StubMailService: MailServicing {
|
||||
private let threadsToLoad: [MailThread]
|
||||
private let sendResult: Result<MailThread, Error>
|
||||
private(set) var sentDrafts: [ComposeDraft] = []
|
||||
|
||||
init(threadsToLoad: [MailThread], sendResult: Result<MailThread, Error> = .failure(TestError.sendFailed)) {
|
||||
self.threadsToLoad = threadsToLoad
|
||||
self.sendResult = sendResult
|
||||
}
|
||||
|
||||
func loadThreads() async throws -> [MailThread] {
|
||||
threadsToLoad
|
||||
}
|
||||
|
||||
func send(draft: ComposeDraft) async throws -> MailThread {
|
||||
sentDrafts.append(draft)
|
||||
return try sendResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
private final class StubControlService: AppControlServicing {
|
||||
private let commandsPerCall: [[AppNavigationCommand]]
|
||||
private var callCount = 0
|
||||
|
||||
init(commandsPerCall: [[AppNavigationCommand]] = []) {
|
||||
self.commandsPerCall = commandsPerCall
|
||||
}
|
||||
|
||||
func commands() -> AsyncStream<AppNavigationCommand> {
|
||||
let commands = callCount < commandsPerCall.count ? commandsPerCall[callCount] : []
|
||||
callCount += 1
|
||||
|
||||
return AsyncStream { continuation in
|
||||
for command in commands {
|
||||
continuation.yield(command)
|
||||
}
|
||||
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeThread(
|
||||
routeID: String,
|
||||
mailbox: Mailbox,
|
||||
subject: String,
|
||||
body: String,
|
||||
isUnread: Bool,
|
||||
isStarred: Bool,
|
||||
sentAt: Date,
|
||||
messageRouteID: String? = nil
|
||||
) -> MailThread {
|
||||
let sender = MailPerson(name: "Sender", email: "sender@social.io")
|
||||
let recipient = MailPerson(name: "Recipient", email: "recipient@social.io")
|
||||
|
||||
return MailThread(
|
||||
routeID: routeID,
|
||||
mailbox: mailbox,
|
||||
subject: subject,
|
||||
participants: [sender, recipient],
|
||||
messages: [
|
||||
MailMessage(
|
||||
routeID: messageRouteID ?? "\(routeID)-message",
|
||||
sender: sender,
|
||||
recipients: [recipient],
|
||||
sentAt: sentAt,
|
||||
body: body
|
||||
)
|
||||
],
|
||||
isUnread: isUnread,
|
||||
isStarred: isStarred,
|
||||
tags: []
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user