168 lines
6.5 KiB
Swift
168 lines
6.5 KiB
Swift
//
|
||
// MessageCenterViewModelTests.swift
|
||
// suixinkanTests
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan_ios
|
||
|
||
@MainActor
|
||
/// 消息中心测试,覆盖分页、筛选、已读和删除。
|
||
final class MessageCenterViewModelTests: XCTestCase {
|
||
func testReloadFirstPageMapsMessagesAndPaginationState() async {
|
||
let api = MessageCenterMock()
|
||
api.listResponses = [
|
||
MessageListResponse(hasMore: true, lastId: 101, items: [
|
||
MessageEntity(id: 101, type: 1, typeName: "", title: "新订单提醒", content: "有新订单", pushAt: "2026-06-25 10:00", isRead: false),
|
||
MessageEntity(id: 100, type: 2, typeName: "核销", title: "", content: "核销完成", pushAt: "2026-06-25 09:00", isRead: true)
|
||
])
|
||
]
|
||
let viewModel = MessageCenterViewModel()
|
||
viewModel.selectedFilter = .unread
|
||
|
||
await viewModel.reloadFirstPage(api: api)
|
||
|
||
XCTAssertEqual(api.listRequests.map(\.unread), [1])
|
||
XCTAssertEqual(viewModel.messages.map(\.id), ["msg_101", "msg_100"])
|
||
XCTAssertEqual(viewModel.messages[0].title, "新订单提醒")
|
||
XCTAssertEqual(viewModel.messages[0].type, .order)
|
||
XCTAssertEqual(viewModel.messages[1].title, "核销")
|
||
XCTAssertEqual(viewModel.messages[1].type, .writeOff)
|
||
XCTAssertEqual(viewModel.unreadCount, 1)
|
||
XCTAssertTrue(viewModel.hasMoreMessages)
|
||
XCTAssertEqual(viewModel.lastId, 101)
|
||
}
|
||
|
||
func testLoadMoreDeduplicatesSortsAndStopsAtTail() async {
|
||
let api = MessageCenterMock()
|
||
api.listResponses = [
|
||
MessageListResponse(hasMore: true, lastId: 101, items: [
|
||
MessageEntity(id: 101, type: 1, title: "A", content: "A", pushAt: "2026-06-25 10:00"),
|
||
MessageEntity(id: 100, type: 3, title: "B", content: "B", pushAt: "2026-06-25 09:00")
|
||
]),
|
||
MessageListResponse(hasMore: false, lastId: 99, items: [
|
||
MessageEntity(id: 100, type: 3, title: "B2", content: "B2", pushAt: "2026-06-25 09:00"),
|
||
MessageEntity(id: 99, type: 0, title: "C", content: "C", pushAt: "2026-06-24 08:00")
|
||
])
|
||
]
|
||
let viewModel = MessageCenterViewModel()
|
||
|
||
await viewModel.reloadFirstPage(api: api)
|
||
await viewModel.loadMore(api: api)
|
||
await viewModel.loadMore(api: api)
|
||
|
||
XCTAssertEqual(api.listRequests.map(\.lastId), [0, 101])
|
||
XCTAssertEqual(viewModel.messages.map(\.id), ["msg_101", "msg_100", "msg_99"])
|
||
XCTAssertEqual(viewModel.messages[1].title, "B2")
|
||
XCTAssertFalse(viewModel.hasMoreMessages)
|
||
XCTAssertEqual(viewModel.lastId, 99)
|
||
}
|
||
|
||
func testReloadFailureClearsStaleMessagesAndPaging() async {
|
||
let api = MessageCenterMock()
|
||
api.listResponses = [
|
||
MessageListResponse(hasMore: true, lastId: 101, items: [
|
||
MessageEntity(id: 101, type: 1, title: "A")
|
||
])
|
||
]
|
||
let viewModel = MessageCenterViewModel()
|
||
await viewModel.reloadFirstPage(api: api)
|
||
|
||
api.listError = TestError.sample
|
||
await viewModel.reloadFirstPage(api: api)
|
||
|
||
XCTAssertTrue(viewModel.messages.isEmpty)
|
||
XCTAssertFalse(viewModel.hasMoreMessages)
|
||
XCTAssertEqual(viewModel.lastId, 0)
|
||
XCTAssertTrue(viewModel.loadFailed)
|
||
}
|
||
|
||
func testMarkReadAllReadAndDeleteUpdateOnlyAfterSuccess() async throws {
|
||
let api = MessageCenterMock()
|
||
api.listResponses = [
|
||
MessageListResponse(items: [
|
||
MessageEntity(id: 101, type: 1, title: "A", isRead: false),
|
||
MessageEntity(id: 100, type: 0, title: "B", isRead: false)
|
||
])
|
||
]
|
||
let viewModel = MessageCenterViewModel()
|
||
await viewModel.reloadFirstPage(api: api)
|
||
|
||
try await viewModel.markAsRead(api: api, item: try XCTUnwrap(viewModel.messages.first))
|
||
XCTAssertEqual(api.readIds, [101])
|
||
XCTAssertEqual(viewModel.messages.map(\.isRead), [true, false])
|
||
|
||
api.readIds = []
|
||
try await viewModel.markAllAsRead(api: api)
|
||
XCTAssertEqual(api.readIds, [100])
|
||
XCTAssertTrue(viewModel.messages.allSatisfy(\.isRead))
|
||
|
||
let item = try XCTUnwrap(viewModel.messages.first)
|
||
try await viewModel.deleteMessage(api: api, item: item)
|
||
XCTAssertEqual(api.deleteIds, [101])
|
||
XCTAssertFalse(viewModel.messages.contains(where: { $0.id == item.id }))
|
||
}
|
||
|
||
func testFailedReadAndDeleteKeepLocalState() async throws {
|
||
let api = MessageCenterMock()
|
||
api.listResponses = [MessageListResponse(items: [MessageEntity(id: 101, type: 1, title: "A", isRead: false)])]
|
||
let viewModel = MessageCenterViewModel()
|
||
await viewModel.reloadFirstPage(api: api)
|
||
let item = try XCTUnwrap(viewModel.messages.first)
|
||
|
||
api.readError = TestError.sample
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.markAsRead(api: api, item: item))
|
||
XCTAssertFalse(try XCTUnwrap(viewModel.messages.first).isRead)
|
||
|
||
api.readError = nil
|
||
api.deleteError = TestError.sample
|
||
await XCTAssertThrowsErrorAsync(try await viewModel.deleteMessage(api: api, item: item))
|
||
XCTAssertEqual(viewModel.messages.map(\.id), [item.id])
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
private final class MessageCenterMock: MessageCenterServing {
|
||
var listResponses: [MessageListResponse] = []
|
||
var listError: Error?
|
||
var readError: Error?
|
||
var deleteError: Error?
|
||
var readIds: [Int] = []
|
||
var deleteIds: [Int] = []
|
||
private(set) var listRequests: [(lastId: Int, limit: Int, unread: Int)] = []
|
||
|
||
func messageList(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||
listRequests.append((lastId, limit, unread))
|
||
if let listError { throw listError }
|
||
return listResponses.isEmpty ? MessageListResponse() : listResponses.removeFirst()
|
||
}
|
||
|
||
func messageRead(id: Int) async throws {
|
||
if let readError { throw readError }
|
||
readIds.append(id)
|
||
}
|
||
|
||
func messageDelete(id: Int) async throws {
|
||
if let deleteError { throw deleteError }
|
||
deleteIds.append(id)
|
||
}
|
||
}
|
||
|
||
private enum TestError: LocalizedError {
|
||
case sample
|
||
var errorDescription: String? { "测试错误" }
|
||
}
|
||
|
||
private func XCTAssertThrowsErrorAsync(
|
||
_ expression: @autoclosure () async throws -> Void,
|
||
file: StaticString = #filePath,
|
||
line: UInt = #line
|
||
) async {
|
||
do {
|
||
try await expression()
|
||
XCTFail("Expected error", file: file, line: line)
|
||
} catch {}
|
||
}
|