feat: 添加云盘与消息中心功能
This commit is contained in:
171
suixinkanTests/MessageCenterViewModelTests.swift
Normal file
171
suixinkanTests/MessageCenterViewModelTests.swift
Normal file
@ -0,0 +1,171 @@
|
||||
//
|
||||
// MessageCenterViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 消息中心 ViewModel 测试。
|
||||
@MainActor
|
||||
final class MessageCenterViewModelTests: XCTestCase {
|
||||
func testInitialLoadAndLoadMoreUseLastIdUntilNoMore() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [
|
||||
MessageListResponse(hasMore: true, lastId: 8, items: [makeMessage(id: 1), makeMessage(id: 8)]),
|
||||
MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 9)]),
|
||||
]
|
||||
let viewModel = MessageCenterViewModel()
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 1, api: api)
|
||||
await viewModel.loadMoreIfNeeded(lastVisibleIndex: 2, api: api)
|
||||
|
||||
XCTAssertEqual(api.listCalls.map(\.lastId), [0, 8])
|
||||
XCTAssertEqual(api.listCalls.map(\.limit), [20, 20])
|
||||
XCTAssertEqual(api.listCalls.map(\.unread), [0, 0])
|
||||
XCTAssertEqual(viewModel.items.map(\.id), [1, 8, 9])
|
||||
XCTAssertFalse(viewModel.canLoadMore)
|
||||
}
|
||||
|
||||
func testUnreadSelectionMarksReadAndOpensDetail() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 3, isRead: false)])]
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var opened: MessageItem?
|
||||
viewModel.onOpenDetail = { opened = $0 }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.selectMessage(id: 3, api: api)
|
||||
|
||||
XCTAssertEqual(api.readCalls, [3])
|
||||
XCTAssertEqual(opened?.id, 3)
|
||||
XCTAssertEqual(opened?.isRead, true)
|
||||
XCTAssertEqual(viewModel.items.first?.isRead, true)
|
||||
}
|
||||
|
||||
func testReadSelectionOpensDetailWithoutCallingReadAPI() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 4, isRead: true)])]
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var opened: MessageItem?
|
||||
viewModel.onOpenDetail = { opened = $0 }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.selectMessage(id: 4, api: api)
|
||||
|
||||
XCTAssertTrue(api.readCalls.isEmpty)
|
||||
XCTAssertEqual(opened?.id, 4)
|
||||
}
|
||||
|
||||
func testUnreadSelectionFailureShowsMessageAndDoesNotOpenDetail() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 5, isRead: false)])]
|
||||
api.readError = TestError(message: "fail")
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var message: String?
|
||||
var didOpen = false
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
viewModel.onOpenDetail = { _ in didOpen = true }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.selectMessage(id: 5, api: api)
|
||||
|
||||
XCTAssertEqual(message, "标记已读失败")
|
||||
XCTAssertFalse(didOpen)
|
||||
XCTAssertEqual(viewModel.items.first?.isRead, false)
|
||||
}
|
||||
|
||||
func testDeleteSuccessRemovesMessageAndReportsDeletedId() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
let listViewModel = MessageCenterViewModel()
|
||||
api.listResponses = [MessageListResponse(items: [makeMessage(id: 7)])]
|
||||
await listViewModel.loadInitial(api: api)
|
||||
|
||||
let detailViewModel = MessageDetailViewModel(message: makeMessage(id: 7))
|
||||
var deletedId: Int?
|
||||
var message: String?
|
||||
detailViewModel.onDeleted = { deletedId = $0 }
|
||||
detailViewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await detailViewModel.delete(api: api)
|
||||
listViewModel.removeMessage(id: deletedId ?? 0)
|
||||
|
||||
XCTAssertEqual(api.deleteCalls, [7])
|
||||
XCTAssertEqual(deletedId, 7)
|
||||
XCTAssertEqual(message, "删除成功")
|
||||
XCTAssertTrue(listViewModel.items.isEmpty)
|
||||
}
|
||||
|
||||
func testDeleteInvalidIdAndFailureMessages() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
let invalidViewModel = MessageDetailViewModel(message: makeMessage(id: 0))
|
||||
var invalidMessage: String?
|
||||
invalidViewModel.onShowMessage = { invalidMessage = $0 }
|
||||
|
||||
await invalidViewModel.delete(api: api)
|
||||
|
||||
XCTAssertEqual(invalidMessage, "消息ID无效")
|
||||
XCTAssertTrue(api.deleteCalls.isEmpty)
|
||||
|
||||
api.deleteError = TestError(message: "server down")
|
||||
let failingViewModel = MessageDetailViewModel(message: makeMessage(id: 9))
|
||||
var failureMessage: String?
|
||||
failingViewModel.onShowMessage = { failureMessage = $0 }
|
||||
|
||||
await failingViewModel.delete(api: api)
|
||||
|
||||
XCTAssertEqual(failureMessage, "删除失败: server down")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class MockMessageCenterAPI: MessageCenterServing {
|
||||
var listResponses: [MessageListResponse] = []
|
||||
var listCalls: [(lastId: Int, limit: Int, unread: Int)] = []
|
||||
var readCalls: [Int] = []
|
||||
var deleteCalls: [Int] = []
|
||||
var readError: Error?
|
||||
var deleteError: Error?
|
||||
|
||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||||
listCalls.append((lastId, limit, unread))
|
||||
guard !listResponses.isEmpty else { return MessageListResponse() }
|
||||
return listResponses.removeFirst()
|
||||
}
|
||||
|
||||
func markAsRead(messageId: Int) async throws {
|
||||
readCalls.append(messageId)
|
||||
if let readError { throw readError }
|
||||
}
|
||||
|
||||
func delete(messageId: Int) async throws {
|
||||
deleteCalls.append(messageId)
|
||||
if let deleteError { throw deleteError }
|
||||
}
|
||||
}
|
||||
|
||||
private struct TestError: LocalizedError {
|
||||
let message: String
|
||||
var errorDescription: String? { message }
|
||||
}
|
||||
|
||||
private func makeMessage(id: Int, isRead: Bool = false) -> MessageItem {
|
||||
MessageItem(
|
||||
id: id,
|
||||
receiverId: 1,
|
||||
receiverType: "staff",
|
||||
type: 1,
|
||||
typeName: "系统通知",
|
||||
title: "系统通知",
|
||||
content: "消息内容",
|
||||
pushAt: "",
|
||||
isRead: isRead,
|
||||
readAt: "",
|
||||
pushChannel: 1,
|
||||
pushChannelName: "站内信",
|
||||
createdAt: "2026-07-09 10:00:00",
|
||||
updatedAt: "2026-07-09 10:00:00"
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user