Files
suixinkan_ios_new/suixinkanTests/OrderDetailViewModelTests.swift
汉秋 c39c3d3c75 新增订单长尾流程,并迁移排队、消息、结算与审核模块
将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:39:02 +08:00

170 lines
7.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrderDetailViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/22.
//
import XCTest
@testable import suixinkan
@MainActor
/// ViewModel
final class OrderDetailViewModelTests: XCTestCase {
/// ID
func testLoadRequestsDetailWhenStoreIdExists() async throws {
let item = try firstOrder()
let api = DetailMockOrderService()
api.detailResponses = [try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success")]
let viewModel = OrderDetailViewModel(item: item)
await viewModel.load(api: api, fallbackStoreId: 66)
XCTAssertEqual(api.detailCalls.count, 1)
XCTAssertEqual(api.detailCalls[0].storeId, 66)
XCTAssertEqual(api.detailCalls[0].orderNumber, "ORDV2001")
XCTAssertEqual(viewModel.display.projectName, "亲子跟拍")
XCTAssertEqual(viewModel.projectInfo?.settleSpotNum, 3)
XCTAssertEqual(viewModel.shootingList.first?.scenicSpotName, "东门")
XCTAssertNil(viewModel.errorMessage)
}
/// ID
func testLoadWithoutStoreIdUsesSummaryOnly() async throws {
let item = try firstOrder()
let api = DetailMockOrderService()
let viewModel = OrderDetailViewModel(item: item)
await viewModel.load(api: api, fallbackStoreId: nil)
XCTAssertTrue(api.detailCalls.isEmpty)
XCTAssertEqual(viewModel.display.orderNumber, "ORDV2001")
XCTAssertEqual(viewModel.contextMessage, "缺少门店上下文,当前展示订单摘要。")
}
///
func testLoadFailureKeepsSummaryAndExposesError() async throws {
let item = try firstOrder()
let api = DetailMockOrderService()
api.detailError = APIError.httpStatus(500, "server error")
let viewModel = OrderDetailViewModel(item: item)
await viewModel.load(api: api, fallbackStoreId: 66)
XCTAssertEqual(api.detailCalls.count, 1)
XCTAssertEqual(viewModel.display.orderNumber, "ORDV2001")
XCTAssertEqual(viewModel.contextMessage, "详情加载失败,当前展示订单摘要。")
XCTAssertNotNil(viewModel.errorMessage)
}
///
func testDetailResponseDecodesMixedNumericAndStringFields() throws {
let detail = try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success")
XCTAssertEqual(detail.orderType, 19)
XCTAssertEqual(detail.actualRefundAmount, "0")
XCTAssertEqual(detail.userId, 7001)
XCTAssertEqual(detail.multiTravel?.projectInfo?.singleSpotVideoNum, 1)
XCTAssertEqual(detail.multiTravel?.shootingList.first?.startAvg, 4.8)
}
/// fixture
private func firstOrder() throws -> OrderEntity {
let payload = try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")
return try XCTUnwrap(payload.list.first)
}
}
@MainActor
/// ViewModel
private final class DetailMockOrderService: OrderServing {
var detailResponses: [StoreOrderDetailResponse] = []
var detailError: Error?
private(set) var detailCalls: [(storeId: Int, orderNumber: 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> {
XCTFail("orderList should not be called")
return ListPayload(total: 0, list: [])
}
///
func writeOffList(scenicId: Int, storeId: Int?, page: Int, pageSize: Int) async throws -> ListPayload<WriteOffOrderItem> {
XCTFail("writeOffList should not be called")
return ListPayload(total: 0, list: [])
}
///
func writeOff(orderNumber: String) async throws {
XCTFail("writeOff should not be called")
}
///
func storeOrderDetail(storeId: Int, orderNumber: String) async throws -> StoreOrderDetailResponse {
detailCalls.append((storeId: storeId, orderNumber: orderNumber))
if let detailError {
throw detailError
}
return detailResponses.isEmpty ? try TestFixture.payload(StoreOrderDetailResponse.self, named: "store_order_detail_success") : detailResponses.removeFirst()
}
///
func depositOrderList(scenicId: Int, page: Int, pageSize: Int) async throws -> ListPayload<DepositOrderListItem> {
XCTFail("depositOrderList should not be called")
return ListPayload(total: 0, list: [])
}
///
func depositOrderWriteOff(orderNumber: String) async throws {
XCTFail("depositOrderWriteOff should not be called")
}
/// 退
func depositOrderRefund(orderNumber: String, refundReason: String) async throws {
XCTFail("depositOrderRefund should not be called")
}
///
func storeOrderShootingDetail(
storeId: Int,
orderNumber: String,
scenicSpotId: Int,
photogUid: Int
) async throws -> StoreOrderShootingDetailResponse {
XCTFail("storeOrderShootingDetail should not be called")
return try TestFixture.payload(StoreOrderShootingDetailResponse.self, named: "store_order_shooting_detail_success")
}
/// 退
func orderRefund(orderNumber: String, refundType: OrderRefundMode, refundAmount: String, refundReason: String) async throws {
XCTFail("orderRefund should not be called")
}
///
func multiTravelShootHistory(orderNumber: String) async throws -> MultiTravelShootHistoryResponse {
XCTFail("multiTravelShootHistory should not be called")
return try TestFixture.payload(MultiTravelShootHistoryResponse.self, named: "multi_travel_shoot_history_success")
}
///
func multiTravelVerifiedScenicSpotList(orderNumber: String) async throws -> [MultiTravelVerifiedScenicSpotItem] {
XCTFail("multiTravelVerifiedScenicSpotList should not be called")
return []
}
///
func multiTravelUploadMaterial(_ request: MultiTravelUploadMaterialRequest) async throws {
XCTFail("multiTravelUploadMaterial should not be called")
}
}