Introduce real payment collection and wallet screens to replace home menu placeholders, wire APIs through RootView, and support bank card OSS uploads plus QR code saving to the photo library. Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
4.7 KiB
Swift
109 lines
4.7 KiB
Swift
//
|
||
// 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()
|
||
}
|
||
}
|