// // PaymentViewModelTests.swift // suixinkanTests // // Created by Codex on 2026/6/22. // import XCTest @testable import suixinkan @MainActor /// 收款模块测试,覆盖收款码、动态二维码、轮询和记录分组。 final class PaymentViewModelTests: XCTestCase { /// 测试无景区时清空二维码且不请求接口。 func testLoadPayCodeWithoutScenicClearsState() async { let api = PaymentMockAPI() let viewModel = PaymentCollectionViewModel() viewModel.staticPayUrl = "old" await viewModel.loadPayCode(api: api, scenicId: nil) XCTAssertTrue(viewModel.staticPayUrl.isEmpty) XCTAssertEqual(api.payCodeScenicIds, []) XCTAssertEqual(viewModel.errorMessage, "请先选择景区") } /// 测试收款码接口加载后生成当前二维码 URL。 func testLoadPayCodeUsesScenicIdAndAppliesStaticURL() async { let api = PaymentMockAPI() api.payCodeResponse = PayCodeResponse(staticPayUrl: "https://pay.example.com/static", dynamicPayUrl: "https://pay.example.com/dynamic") let viewModel = PaymentCollectionViewModel() await viewModel.loadPayCode(api: api, scenicId: 9) XCTAssertEqual(api.payCodeScenicIds, [9]) XCTAssertEqual(viewModel.currentPayUrl, "https://pay.example.com/static") XCTAssertTrue(viewModel.hasStaticPayCode) } /// 测试设置金额会校验金额并生成动态收款 URL。 func testApplyDynamicAmountBuildsDynamicURL() async { let viewModel = PaymentCollectionViewModel() viewModel.dynamicPayUrl = "https://pay.example.com/dynamic?scenic_id=9" viewModel.amountText = "12.345" viewModel.remarkText = "茶水" XCTAssertTrue(viewModel.applyDynamicAmount()) XCTAssertTrue(viewModel.currentPayUrl.contains("amount=12.35")) XCTAssertTrue(viewModel.currentPayUrl.contains("remark=")) XCTAssertEqual(viewModel.status, .waiting) } /// 测试轮询命中新收款记录后进入成功态。 func testPollingFindsNewPaymentRecord() async { let api = PaymentMockAPI() api.recordResponses = [ PaymentCollectionRecordResponse(list: [ PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-22", createTime: "10:00") ]), PaymentCollectionRecordResponse(list: [ PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-22", createTime: "10:00"), PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.35", createDate: "2026-06-22", createTime: "10:01") ]) ] let viewModel = PaymentCollectionViewModel() viewModel.amountText = "12.35" await viewModel.primePaymentRecords(api: api, scenicId: 9) await viewModel.pollUntilPaymentDetected(api: api, scenicId: 9, maxAttempts: 1, intervalNanoseconds: 0) XCTAssertEqual(viewModel.status, .success(PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.35", createDate: "2026-06-22", createTime: "10:01"))) } /// 测试后端缺少日汇总时能从列表兜底生成分组。 func testRecordGroupsFallbackFromList() { let response = PaymentCollectionRecordResponse(list: [ PaymentCollectionRecordItem(orderNumber: "A", userPhone: "138", orderAmount: "8.00", createDate: "2026-06-21", createTime: "10:00"), PaymentCollectionRecordItem(orderNumber: "B", userPhone: "139", orderAmount: "12.00", createDate: "2026-06-21", createTime: "10:01") ]) let groups = PaymentCollectionRecordViewModel.makeGroups(from: response) XCTAssertEqual(groups.count, 1) XCTAssertEqual(groups[0].analyse.orderCount, 2) XCTAssertEqual(groups[0].analyse.orderAmountSum, "20") } } @MainActor /// 收款 API 测试替身。 private final class PaymentMockAPI: PaymentServing { var payCodeResponse = PayCodeResponse() var recordResponses: [PaymentCollectionRecordResponse] = [] private(set) var payCodeScenicIds: [Int] = [] private(set) var recordScenicIds: [Int] = [] /// 记录景区 ID 并返回收款码响应。 func payCode(scenicId: Int) async throws -> PayCodeResponse { payCodeScenicIds.append(scenicId) return payCodeResponse } /// 记录景区 ID 并返回下一份收款记录响应。 func paymentCollectionRecords(scenicId: Int) async throws -> PaymentCollectionRecordResponse { recordScenicIds.append(scenicId) return recordResponses.isEmpty ? PaymentCollectionRecordResponse() : recordResponses.removeFirst() } }