// // MainTabBadgeViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/23. // import XCTest @testable import suixinkan @MainActor /// 主 Tab 角标测试,覆盖待核销数量加载、封顶和失败清理。 final class MainTabBadgeViewModelTests: XCTestCase { /// 测试无景区时清空角标且不请求接口。 func testMissingScenicClearsBadgeWithoutRequestingAPI() async { let api = MockMainTabOrderService() let viewModel = MainTabBadgeViewModel() await viewModel.refreshPendingWriteOffCount(api: api, scenicId: nil, storeId: 66) XCTAssertNil(viewModel.pendingWriteOffCount) XCTAssertTrue(api.writeOffListCalls.isEmpty) } /// 测试接口成功时展示待核销总数。 func testSuccessfulRefreshStoresPendingWriteOffCount() async { let api = MockMainTabOrderService() api.writeOffResponse = ListPayload(total: 8, list: []) let viewModel = MainTabBadgeViewModel() await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: 66) XCTAssertEqual(viewModel.pendingWriteOffCount, 8) XCTAssertEqual(api.writeOffListCalls, [.init(scenicId: 88, storeId: 66, page: 1, pageSize: 1)]) } /// 测试待核销数量超过 99 时角标封顶。 func testSuccessfulRefreshCapsPendingWriteOffCountAtNinetyNine() async { let api = MockMainTabOrderService() api.writeOffResponse = ListPayload(total: 120, list: []) let viewModel = MainTabBadgeViewModel() await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: nil) XCTAssertEqual(viewModel.pendingWriteOffCount, 99) } /// 测试接口失败时清空角标。 func testRefreshFailureClearsBadge() async { let api = MockMainTabOrderService() api.writeOffError = APIError.httpStatus(500, "server error") let viewModel = MainTabBadgeViewModel() await viewModel.refreshPendingWriteOffCount(api: api, scenicId: 88, storeId: 66) XCTAssertNil(viewModel.pendingWriteOffCount) XCTAssertEqual(api.writeOffListCalls.count, 1) } } @MainActor /// 主 Tab 角标测试用订单服务,只实现角标依赖的核销列表记录。 private final class MockMainTabOrderService: OrderServing { struct WriteOffListCall: Equatable { let scenicId: Int let storeId: Int? let page: Int let pageSize: Int } var writeOffResponse = ListPayload(total: 0, list: []) var writeOffError: Error? private(set) var writeOffListCalls: [WriteOffListCall] = [] /// 返回空订单管理列表,角标测试不会使用该方法。 func orderList( scenicId: Int, page: Int, pageSize: Int, orderStatus: Int?, userPhone: String?, startTime: String?, endTime: String?, isRefined: Int?, isScenicAdmin: Bool ) async throws -> ListPayload { ListPayload(total: 0, list: []) } /// 记录核销列表请求并返回预设响应。 func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload { writeOffListCalls.append(.init(scenicId: scenicId, storeId: storeId, page: page, pageSize: pageSize)) if let writeOffError { throw writeOffError } return writeOffResponse } /// 角标测试不会触发核销操作。 func writeOff(orderNumber: String) async throws {} /// 角标测试不会请求订单详情。 func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse { try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") } /// 角标测试不会请求押金订单列表。 func depositOrderList(scenicId: Int, page: Int, pageSize: Int) async throws -> ListPayload { ListPayload(total: 0, list: []) } /// 角标测试不会触发押金核销。 func depositOrderWriteOff(orderNumber: String) async throws {} /// 角标测试不会触发押金退款。 func depositOrderRefund(orderNumber: String, refundReason: String) async throws {} /// 角标测试不会请求拍摄信息。 func storeOrderShootingDetail( storeId: Int, orderNumber: String, scenicSpotId: Int, photogUid: Int ) async throws -> StoreOrderShootingDetailResponse { try TestFixture.payload(StoreOrderShootingDetailResponse.self, named: "store_order_shooting_detail_success") } /// 角标测试不会触发普通退款。 func orderRefund(orderNumber: String, refundType: OrderRefundMode, refundAmount: String, refundReason: String) async throws {} /// 角标测试不会请求历史拍摄。 func multiTravelShootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse { try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success") } /// 角标测试不会请求已核销打卡点。 func multiTravelVerifiedScenicSpotList(orderNumber: String) async throws -> [MultiTravelVerifiedScenicSpotItem] { [] } /// 角标测试不会提交多点旅拍素材。 func multiTravelUploadMaterial(_ request: MultiTravelUploadMaterialRequest) async throws {} }