223 lines
8.3 KiB
Swift
223 lines
8.3 KiB
Swift
//
|
||
// WriteOffListViewModelTests.swift
|
||
// suixinkanTests
|
||
//
|
||
// Created by Codex on 2026/6/29.
|
||
//
|
||
|
||
import XCTest
|
||
@testable import suixinkan
|
||
|
||
@MainActor
|
||
/// 核销管理页 ViewModel 测试,覆盖筛选、分页与核销流程。
|
||
final class WriteOffListViewModelTests: XCTestCase {
|
||
/// 测试无景区时清空列表且不请求接口。
|
||
func testReloadClearsDataWhenScenicIsMissing() async throws {
|
||
let api = WriteOffMockOrderService()
|
||
api.responses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
|
||
let viewModel = WriteOffListViewModel()
|
||
|
||
try await viewModel.reload(api: api, scenicId: 88, storeId: 66)
|
||
XCTAssertFalse(viewModel.orders.isEmpty)
|
||
|
||
try await viewModel.reload(api: api, scenicId: nil)
|
||
|
||
XCTAssertTrue(viewModel.orders.isEmpty)
|
||
XCTAssertEqual(viewModel.total, 0)
|
||
XCTAssertEqual(api.listCalls.count, 1)
|
||
}
|
||
|
||
/// 测试状态 Tab 与搜索参数会传入服务层。
|
||
func testReloadUsesStatusAndPhoneFilters() async throws {
|
||
let api = WriteOffMockOrderService()
|
||
api.responses = [
|
||
try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success"),
|
||
try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")
|
||
]
|
||
let viewModel = WriteOffListViewModel()
|
||
viewModel.searchPhone = "13800000000"
|
||
viewModel.selectedStatus = 18
|
||
viewModel.filterStartDate = Self.date("2026-05-01")
|
||
viewModel.filterEndDate = Self.date("2026-05-31")
|
||
|
||
try await viewModel.reload(api: api, scenicId: 88, storeId: 66)
|
||
|
||
XCTAssertEqual(api.listCalls[0].orderStatus, 18)
|
||
XCTAssertNil(api.listCalls[0].orderVerifyStatus)
|
||
XCTAssertEqual(api.listCalls[0].userPhone, "13800000000")
|
||
XCTAssertEqual(api.listCalls[0].startTime, "2026-05-01")
|
||
XCTAssertEqual(api.listCalls[0].endTime, "2026-05-31")
|
||
|
||
viewModel.selectedStatus = 1
|
||
try await viewModel.reload(api: api, scenicId: 88)
|
||
|
||
XCTAssertNil(api.listCalls[1].orderStatus)
|
||
XCTAssertEqual(api.listCalls[1].orderVerifyStatus, 1)
|
||
}
|
||
|
||
/// 测试核销成功后刷新列表。
|
||
func testVerifyRefreshesListAfterSuccess() async throws {
|
||
let api = WriteOffMockOrderService()
|
||
api.responses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
|
||
let viewModel = WriteOffListViewModel()
|
||
|
||
try await viewModel.verify(api: api, scenicId: 88, storeId: 66, orderNumber: "VERIFY001")
|
||
|
||
XCTAssertEqual(api.verifiedOrderNumbers, ["VERIFY001"])
|
||
XCTAssertEqual(api.listCalls.count, 1)
|
||
XCTAssertEqual(viewModel.orders.count, 1)
|
||
XCTAssertFalse(viewModel.isVerifying)
|
||
}
|
||
|
||
/// 测试扫码命中当前列表时返回匹配订单。
|
||
func testMatchedWriteOffOrderFindsCurrentListItem() async throws {
|
||
let api = WriteOffMockOrderService()
|
||
api.responses = [try TestFixture.payload(ListPayload<WriteOffOrderItem>.self, named: "writeoff_list_success")]
|
||
let viewModel = WriteOffListViewModel()
|
||
try await viewModel.reload(api: api, scenicId: 88)
|
||
|
||
let result = try XCTUnwrap(viewModel.matchedWriteOffOrder(for: "order_number=VERIFY001"))
|
||
XCTAssertEqual(result.orderNumber, "VERIFY001")
|
||
XCTAssertEqual(result.matched?.orderNumber, "VERIFY001")
|
||
}
|
||
|
||
/// 测试 canVerify 会排除已核销状态。
|
||
func testCanVerifyExcludesVerifiedStatus() throws {
|
||
let viewModel = WriteOffListViewModel()
|
||
let verified = try JSONDecoder().decode(
|
||
WriteOffOrderItem.self,
|
||
from: Data(
|
||
"""
|
||
{
|
||
"order_number": "V2",
|
||
"order_verification_status": "已核销",
|
||
"pay_time": "",
|
||
"project_name": "",
|
||
"user_phone": "",
|
||
"order_amount": "",
|
||
"order_status_name": "",
|
||
"order_verification_time": ""
|
||
}
|
||
""".utf8
|
||
)
|
||
)
|
||
XCTAssertFalse(viewModel.canVerify(verified))
|
||
}
|
||
|
||
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
|
||
/// 核销列表测试替身,记录 writeOffList 与核销调用。
|
||
private final class WriteOffMockOrderService: OrderServing {
|
||
struct ListCall: Equatable {
|
||
let scenicId: Int
|
||
let storeId: Int?
|
||
let page: Int
|
||
let orderStatus: Int?
|
||
let orderVerifyStatus: Int?
|
||
let startTime: String?
|
||
let endTime: String?
|
||
let userPhone: String?
|
||
}
|
||
|
||
var responses: [ListPayload<WriteOffOrderItem>] = []
|
||
private(set) var listCalls: [ListCall] = []
|
||
private(set) var verifiedOrderNumbers: [String] = []
|
||
|
||
func orderList(
|
||
scenicId: Int,
|
||
page: Int,
|
||
pageSize: Int,
|
||
orderStatus: Int?,
|
||
userPhone: String?,
|
||
startTime: String?,
|
||
endTime: String?,
|
||
isRefined: Int?,
|
||
isScenicAdmin: Bool
|
||
) async throws -> ListPayload<OrderEntity> {
|
||
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<WriteOffOrderItem> {
|
||
listCalls.append(
|
||
ListCall(
|
||
scenicId: scenicId,
|
||
storeId: storeId,
|
||
page: page,
|
||
orderStatus: orderStatus,
|
||
orderVerifyStatus: orderVerifyStatus,
|
||
startTime: startTime,
|
||
endTime: endTime,
|
||
userPhone: userPhone
|
||
)
|
||
)
|
||
return responses.isEmpty ? ListPayload(total: 0, list: []) : responses.removeFirst()
|
||
}
|
||
|
||
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<StoreOrderListItem> {
|
||
ListPayload(total: 0, list: [])
|
||
}
|
||
func storeOrderRefund(
|
||
storeId: Int,
|
||
orderNumber: String,
|
||
refundType: OrderRefundMode,
|
||
refundAmount: String,
|
||
refundReason: String
|
||
) async throws {}
|
||
|
||
func writeOff(orderNumber: String) async throws {
|
||
verifiedOrderNumbers.append(orderNumber)
|
||
}
|
||
|
||
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<DepositOrderListItem> {
|
||
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 {}
|
||
}
|