// // ScenicSettlementViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/25. // import XCTest @testable import suixinkan @MainActor /// 景区结算测试,覆盖结算申请和审核记录聚合。 final class ScenicSettlementViewModelTests: XCTestCase { /// 测试已有景区会从可申请结算景区中排除。 func testSettlementLoadExcludesExistingScenics() async { let permissionAPI = ScenicSettlementPermissionMock() permissionAPI.scenicList = ScenicListAllResponse(total: 3, list: [ ScenicListItem(id: 1, name: "已有景区"), ScenicListItem(id: 2, name: "新增景区"), ScenicListItem(id: 3, name: "另一个景区") ]) let viewModel = ScenicSettlementViewModel() await viewModel.load( api: permissionAPI, existingScenics: [BusinessScope(id: 1, name: "已有景区", kind: .scenic)] ) XCTAssertEqual(viewModel.options.map(\.id), [2, 3]) } /// 测试金额校验覆盖空值、0、负数、超过两位小数和有效金额。 func testSettlementAmountValidation() { let viewModel = ScenicSettlementViewModel() XCTAssertEqual(viewModel.amountValidation, .empty) viewModel.amountText = "0" XCTAssertEqual(viewModel.amountValidation, .notPositive) viewModel.amountText = "-1" XCTAssertEqual(viewModel.amountValidation, .invalidFormat) viewModel.amountText = "1.234" XCTAssertEqual(viewModel.amountValidation, .invalidFormat) viewModel.amountText = "12.30" XCTAssertEqual(viewModel.amountValidation, .valid) } /// 测试多景区提交 body 正确且成功后清空表单。 func testSettlementSubmitMultipleScenicsAndClearsForm() async { let submitAPI = ScenicSettlementSubmitMock() let viewModel = ScenicSettlementViewModel() viewModel.options = [ ScenicSettlementOption(id: 3, name: "三号", selected: false), ScenicSettlementOption(id: 2, name: "二号", selected: false) ] viewModel.toggleScenic(id: 3) viewModel.toggleScenic(id: 2) viewModel.amountText = "99" viewModel.remarkText = " 活动结算 " let success = await viewModel.submit(api: submitAPI) XCTAssertTrue(success) XCTAssertEqual(submitAPI.requests, [ ScenicSettlementSubmitRequest(scenicId: 2, applyAmount: "99.00", applyRemark: "活动结算"), ScenicSettlementSubmitRequest(scenicId: 3, applyAmount: "99.00", applyRemark: "活动结算") ]) XCTAssertTrue(viewModel.selectedScenicIds.isEmpty) XCTAssertEqual(viewModel.amountText, "") XCTAssertEqual(viewModel.remarkText, "") XCTAssertEqual(viewModel.message, "提交成功,等待审核") } /// 测试任一提交失败会保留当前表单并透出错误。 func testSettlementSubmitFailureKeepsForm() async { let submitAPI = ScenicSettlementSubmitMock() submitAPI.errorAfterRequestCount = 1 let viewModel = ScenicSettlementViewModel() viewModel.options = [ ScenicSettlementOption(id: 1, name: "一号", selected: false), ScenicSettlementOption(id: 2, name: "二号", selected: false) ] viewModel.toggleScenic(id: 1) viewModel.toggleScenic(id: 2) viewModel.amountText = "10" let success = await viewModel.submit(api: submitAPI) XCTAssertFalse(success) XCTAssertEqual(viewModel.selectedScenicIds, [1, 2]) XCTAssertEqual(viewModel.amountText, "10") XCTAssertEqual(viewModel.message, TestError.sample.localizedDescription) } /// 测试审核记录两个通道都成功时汇总正确。 func testReviewLoadSuccessAggregatesRecords() async { let permissionAPI = ScenicSettlementPermissionMock() permissionAPI.scenicPendings = ScenicApplicationPendingsResponse(items: [ ScenicApplicationPendingResponse(id: 1, scenicName: "景区", status: 1) ]) permissionAPI.roleApplies = [ RoleApplyPendingResponse(id: 2, roleId: 1, roleName: "摄影师", status: 2) ] let viewModel = ScenicSettlementReviewViewModel() await viewModel.load(api: permissionAPI) XCTAssertEqual(viewModel.scenicApplications.count, 1) XCTAssertEqual(viewModel.roleApplications.count, 1) XCTAssertEqual(viewModel.pendingCount, 1) XCTAssertFalse(viewModel.loadFailedAll) } /// 测试单通道失败时保留另一通道数据。 func testReviewSingleChannelFailureKeepsOtherChannel() async { let permissionAPI = ScenicSettlementPermissionMock() permissionAPI.scenicPendingsError = TestError.sample permissionAPI.roleApplies = [ RoleApplyPendingResponse(id: 2, roleId: 1, roleName: "摄影师", status: 1) ] let viewModel = ScenicSettlementReviewViewModel() await viewModel.load(api: permissionAPI) XCTAssertTrue(viewModel.scenicLoadFailed) XCTAssertFalse(viewModel.roleLoadFailed) XCTAssertFalse(viewModel.loadFailedAll) XCTAssertEqual(viewModel.roleApplications.count, 1) } /// 测试双通道失败时进入整页失败态。 func testReviewBothChannelsFailureMarksFullFailure() async { let permissionAPI = ScenicSettlementPermissionMock() permissionAPI.scenicPendingsError = TestError.sample permissionAPI.roleAppliesError = TestError.sample let viewModel = ScenicSettlementReviewViewModel() await viewModel.load(api: permissionAPI) XCTAssertTrue(viewModel.loadFailedAll) XCTAssertTrue(viewModel.scenicApplications.isEmpty) XCTAssertTrue(viewModel.roleApplications.isEmpty) } /// 测试审核状态文案和语义映射。 func testReviewStatusMapping() { let viewModel = ScenicSettlementReviewViewModel() XCTAssertEqual(viewModel.statusText(1), "待审核") XCTAssertEqual(viewModel.statusText(2), "已通过") XCTAssertEqual(viewModel.statusText(3), "已驳回") XCTAssertEqual(viewModel.statusText(9), "已取消") XCTAssertEqual(viewModel.statusText(99), "未知") XCTAssertEqual(viewModel.statusIcon(2), "checkmark.circle.fill") } } @MainActor private final class ScenicSettlementPermissionMock: ScenicPermissionServing { var scenicList = ScenicListAllResponse(total: 0, list: []) var scenicListError: Error? var scenicPendings = ScenicApplicationPendingsResponse() var scenicPendingsError: Error? var roleApplies: [RoleApplyPendingResponse] = [] var roleAppliesError: Error? func scenicListAll() async throws -> ScenicListAllResponse { if let scenicListError { throw scenicListError } return scenicList } func areas() async throws -> [ScenicAreaNode] { [] } func scenicApplicationPendingAll() async throws -> ScenicApplicationPendingsResponse { if let scenicPendingsError { throw scenicPendingsError } return scenicPendings } func scenicSubmit(_ request: ScenicApplicationSubmitRequest) async throws {} func scenicApplicationUploadPlaceholder(_ items: [ScenicApplicationUploadPlaceholder]) async throws {} func roleApplyAll() async throws -> [RoleApplyPendingResponse] { if let roleAppliesError { throw roleAppliesError } return roleApplies } func roleApplySubmit(roleId: Int, scenicIds: [Int]) async throws {} } @MainActor private final class ScenicSettlementSubmitMock: ScenicSettlementServing { var requests: [ScenicSettlementSubmitRequest] = [] var errorAfterRequestCount: Int? func scenicSettlementSubmit(_ request: ScenicSettlementSubmitRequest) async throws { requests.append(request) if let errorAfterRequestCount, requests.count >= errorAfterRequestCount { throw TestError.sample } } } private enum TestError: LocalizedError { case sample var errorDescription: String? { "测试错误" } }