统一收款详情布局、设置金额弹窗和收款记录样式,接入 type 6/1 推送驱动与轮询 fallback,使 iOS 收款反馈与 Android 一致。 Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
7.9 KiB
Swift
183 lines
7.9 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 = makeViewModel()
|
||
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 = makeViewModel()
|
||
|
||
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 = makeViewModel()
|
||
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 testConfirmAmountUpdatesQRCode() {
|
||
let viewModel = makeViewModel()
|
||
viewModel.staticPayUrl = "https://pay.example.com/static"
|
||
viewModel.dynamicPayUrl = "https://pay.example.com/dynamic?scenic_id=9"
|
||
viewModel.amountText = "18.50"
|
||
viewModel.remarkText = "门票"
|
||
|
||
XCTAssertTrue(viewModel.confirmAmount())
|
||
XCTAssertTrue(viewModel.currentPayUrl.contains("amount=18.5"))
|
||
XCTAssertEqual(viewModel.status, .waiting)
|
||
XCTAssertNil(viewModel.scanSuccessTime)
|
||
XCTAssertNil(viewModel.paymentSuccessData)
|
||
}
|
||
|
||
/// 测试扫码成功推送会写入扫码时间并清空付款成功态。
|
||
func testHandlePaymentPushScanSuccess() {
|
||
let viewModel = makeViewModel()
|
||
viewModel.paymentSuccessData = PaymentSuccessDisplayData(
|
||
orderAmount: "10",
|
||
orderNumber: "A",
|
||
payTime: "2026-06-22 10:00",
|
||
remark: ""
|
||
)
|
||
|
||
viewModel.handlePaymentPush(type: 6, dataJSON: nil)
|
||
|
||
XCTAssertNotNil(viewModel.scanSuccessTime)
|
||
XCTAssertNil(viewModel.paymentSuccessData)
|
||
}
|
||
|
||
/// 测试付款成功推送会解析详情并清空扫码态。
|
||
func testHandlePaymentPushPaymentSuccess() {
|
||
let viewModel = makeViewModel()
|
||
viewModel.scanSuccessTime = "2026-06-22 10:00"
|
||
let payload = """
|
||
{"order_amount":"88.00","order_number":"ORDER-1","pay_time":"2026-06-22 10:01","remark":"测试"}
|
||
"""
|
||
|
||
viewModel.handlePaymentPush(type: 1, dataJSON: payload)
|
||
|
||
XCTAssertNil(viewModel.scanSuccessTime)
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.orderAmount, "88.00")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.orderNumber, "ORDER-1")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.payTime, "2026-06-22 10:01")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.remark, "测试")
|
||
}
|
||
|
||
/// 测试语音开关会写入偏好存储。
|
||
func testToggleVoiceBroadcastPersistsPreference() {
|
||
let defaults = UserDefaults(suiteName: "PaymentViewModelTests.voice")!
|
||
defaults.removePersistentDomain(forName: "PaymentViewModelTests.voice")
|
||
let preferences = AppPreferencesStore(defaults: defaults)
|
||
let viewModel = makeViewModel(preferencesStore: preferences)
|
||
|
||
XCTAssertFalse(viewModel.isVoiceBroadcast)
|
||
viewModel.toggleVoiceBroadcast()
|
||
XCTAssertTrue(viewModel.isVoiceBroadcast)
|
||
XCTAssertTrue(preferences.loadPaymentReceiveVoiceEnabled())
|
||
|
||
let reloaded = makeViewModel(preferencesStore: preferences)
|
||
XCTAssertTrue(reloaded.isVoiceBroadcast)
|
||
}
|
||
|
||
/// 测试轮询命中新收款记录后进入成功态并填充详情。
|
||
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 = makeViewModel()
|
||
viewModel.amountText = "12.35"
|
||
viewModel.remarkText = "备注"
|
||
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")))
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.orderAmount, "12.35")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.orderNumber, "B")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.payTime, "2026-06-22 10:01")
|
||
XCTAssertEqual(viewModel.paymentSuccessData?.remark, "备注")
|
||
}
|
||
|
||
/// 测试后端缺少日汇总时能从列表兜底生成分组。
|
||
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")
|
||
}
|
||
|
||
/// 创建测试用 ViewModel,并允许注入独立偏好存储。
|
||
private func makeViewModel(preferencesStore: AppPreferencesStore = AppPreferencesStore(defaults: UserDefaults(suiteName: "PaymentViewModelTests.default")!)) -> PaymentCollectionViewModel {
|
||
PaymentCollectionViewModel(preferencesStore: preferencesStore)
|
||
}
|
||
}
|
||
|
||
@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()
|
||
}
|
||
}
|