// // StoreAdminOrderListViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/29. // import XCTest @testable import suixinkan @MainActor /// 店长订单 Tab ViewModel 测试,覆盖列表分页、筛选与退款参数。 final class StoreAdminOrderListViewModelTests: XCTestCase { /// 测试无门店时清空列表且不请求接口。 func testReloadClearsDataWhenStoreIsMissing() async throws { let api = StoreAdminMockOrderService() api.responses = [try TestFixture.payload(ListPayload.self, named: "store_order_list_success")] let viewModel = StoreAdminOrderListViewModel() try await viewModel.reload(api: api, storeId: 66) XCTAssertFalse(viewModel.orders.isEmpty) try await viewModel.reload(api: api, storeId: nil) XCTAssertTrue(viewModel.orders.isEmpty) XCTAssertEqual(viewModel.total, 0) XCTAssertEqual(viewModel.page, 1) XCTAssertFalse(viewModel.hasMore) XCTAssertEqual(api.listCalls.count, 1) } /// 测试筛选参数会传入服务层并重置到第一页。 func testReloadUsesFilterQueries() async throws { let api = StoreAdminMockOrderService() api.responses = [ try TestFixture.payload(ListPayload.self, named: "store_order_list_success"), try TestFixture.payload(ListPayload.self, named: "store_order_list_success") ] let viewModel = StoreAdminOrderListViewModel() viewModel.searchPhone = " 13800000000 " viewModel.selectedStatus = 30 viewModel.filterStartDate = Self.date("2026-05-01") viewModel.filterEndDate = Self.date("2026-05-31") try await viewModel.reload(api: api, storeId: 66) XCTAssertEqual(api.listCalls.count, 1) XCTAssertEqual(api.listCalls[0].storeId, 66) XCTAssertEqual(api.listCalls[0].userPhone, "13800000000") XCTAssertEqual(api.listCalls[0].orderStatus, 30) XCTAssertEqual(api.listCalls[0].startTime, "2026-05-01") XCTAssertEqual(api.listCalls[0].endTime, "2026-05-31") viewModel.selectedStatus = 0 viewModel.searchPhone = "" try await viewModel.reload(api: api, storeId: 66) XCTAssertEqual(api.listCalls[1].orderStatus, nil) XCTAssertNil(api.listCalls[1].userPhone) } /// 测试最后一页不会继续请求。 func testLoadMoreStopsAfterLastPage() async throws { let api = StoreAdminMockOrderService() api.responses = [try TestFixture.payload(ListPayload.self, named: "store_order_list_success")] let viewModel = StoreAdminOrderListViewModel() try await viewModel.reload(api: api, storeId: 66) try await viewModel.loadMore(api: api, storeId: 66) XCTAssertEqual(viewModel.orders.count, 2) XCTAssertEqual(api.listCalls.count, 1) XCTAssertFalse(viewModel.hasMore) } /// 测试店长退款会传入门店与订单参数。 func testRefundUsesStoreParameters() async throws { let api = StoreAdminMockOrderService() let viewModel = StoreAdminOrderListViewModel() try await viewModel.refund( api: api, storeId: 66, orderNumber: "STORE001", mode: .partial, amount: "50.00", reason: "部分退款" ) XCTAssertEqual(api.refundCalls.count, 1) XCTAssertEqual(api.refundCalls[0].storeId, 66) XCTAssertEqual(api.refundCalls[0].orderNumber, "STORE001") XCTAssertEqual(api.refundCalls[0].mode, .partial) XCTAssertEqual(api.refundCalls[0].amount, "50.00") XCTAssertEqual(api.refundCalls[0].reason, "部分退款") } private static func date(_ text: String) -> Date { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: text)! } } @MainActor /// 店长订单列表测试替身,记录 storeOrderList 与退款调用。 private final class StoreAdminMockOrderService: OrderServing { struct ListCall: Equatable { let storeId: Int? let page: Int let userPhone: String? let orderStatus: Int? let startTime: String? let endTime: String? } struct RefundCall: Equatable { let storeId: Int let orderNumber: String let mode: OrderRefundMode let amount: String let reason: String } var responses: [ListPayload] = [] private(set) var listCalls: [ListCall] = [] private(set) var refundCalls: [RefundCall] = [] 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, orderStatus: Int?, orderVerifyStatus: Int?, startTime: String?, endTime: String?, userPhone: String? ) async throws -> ListPayload { ListPayload(total: 0, list: []) } func updateRefinedChoice(orderNumber: String, isRefined: Int) async throws {} func cancelOrder(orderNumber: String) async throws {} func sendGift(orderNumber: String, photoNum: Int, videoNum: Int) async throws {} func storeOrderList( storeId: Int?, page: Int, pageSize: Int, userPhone: String?, orderStatus: Int?, orderType: Int?, startTime: String?, endTime: String? ) async throws -> ListPayload { listCalls.append( ListCall( storeId: storeId, page: page, userPhone: userPhone, orderStatus: orderStatus, startTime: startTime, endTime: endTime ) ) return responses.isEmpty ? ListPayload(total: 0, list: []) : responses.removeFirst() } func storeOrderRefund( storeId: Int, orderNumber: String, refundType: OrderRefundMode, refundAmount: String, refundReason: String ) async throws { refundCalls.append( RefundCall( storeId: storeId, orderNumber: orderNumber, mode: refundType, amount: refundAmount, reason: refundReason ) ) } 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 {} }